char-slop/ai-dots
ai dotfiles
git clone https://git.t4t.associates/char-slop/ai-dots
0331a75
main
1import type { Api , Model } from "@earendil-works/pi-ai" ; 2import { openrouterProvider } from "@earendil-works/pi-ai/providers/openrouter" ; 3import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" ; 4 5const CATALOG_URL = "https://openrouter.ai/api/v1/models" ; 6const REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000 ; 7const CACHE_MARKER = "openrouter-direct" ; 8 9type OpenRouterModel = { 10id :string ; 11name :string ; 12context_length ?:number ; 13architecture ?:{ input_modalities ?:string []; output_modalities ?:string []; modality ?:string }; 14pricing ?:{ prompt ?:string ; completion ?:string ; input_cache_read ?:string ; input_cache_write ?:string }; 15top_provider ?:{ context_length ?:number ; max_completion_tokens ?:number }; 16supported_parameters ?:string []; 17reasoning ?:{ mandatory ?:boolean ; supported_efforts ?:string [] }; 18}; 19 20function catalogModel ( entry :OpenRouterModel ) :Model < Api > | undefined { 21if ( ! entry . id || ! entry . name ) return ; 22if ( entry . architecture ?. output_modalities && ! entry . architecture . output_modalities . includes ( "text" )) return ; 23if ( entry . architecture ?. input_modalities && ! entry . architecture . input_modalities . includes ( "text" )) return ; 24 25const rate = ( value :string | undefined ) :number => { 26const cost = Number ( value ?? 0 ) * 1_000_000 ; 27return Number . isFinite ( cost ) && cost >= 0 ?Number ( cost . toFixed ( 6 )) :0 ; 28}; 29const efforts = entry . reasoning ?. supported_efforts ; 30const thinkingLevelMap = efforts ?. length 31 ?{ 32off :entry . reasoning ?. mandatory ?null :"none" , 33 ...Object . fromEntries ( 34([ "minimal" , "low" , "medium" , "high" , "xhigh" , "max" ] as const ). map (( level ) => [ 35level , 36efforts . includes ( level ) ?level :null , 37]), 38), 39} 40 :entry . reasoning ?. mandatory ?{ off :null } :undefined ; 41const anthropic = entry . id . startsWith ( "anthropic/" ) && ! entry . id . endsWith ( ":batch" ); 42return { 43id :entry . id , 44name :entry . name , 45provider :"openrouter" , 46api :anthropic ?"anthropic-messages" :"openai-completions" , 47baseUrl :anthropic ?"https://openrouter.ai/api" :"https://openrouter.ai/api/v1" , 48reasoning :entry . supported_parameters ?. includes ( "reasoning" ) ?? false , 49 ...( thinkingLevelMap && { thinkingLevelMap}), 50input :entry . architecture ?. input_modalities ?. includes ( "image" ) || entry . architecture ?. modality ?. includes ( "image" ) 51 ?[ "text" , "image" ] 52 :[ "text" ], 53cost :{ 54input :rate ( entry . pricing ?. prompt ), 55output :rate ( entry . pricing ?. completion ), 56cacheRead :rate ( entry . pricing ?. input_cache_read ), 57cacheWrite :rate ( entry . pricing ?. input_cache_write ), 58}, 59contextWindow :entry . top_provider ?. context_length || entry . context_length || 4096 , 60maxTokens :entry . top_provider ?. max_completion_tokens || 4096 , 61}; 62} 63 64export default function ( pi :ExtensionAPI ) { 65const builtIn = openrouterProvider (); 66let liveModels :readonly Model < Api >[] = []; 67 68pi . registerProvider ({ 69 ...builtIn , 70getModels :() => { 71// Keep Pi's hand-tuned settings for known models; add models missing from its catalog. 72const models = new Map < string , Model < Api >>( liveModels . map (( model ) => [ model . id , model ])); 73for ( const model of builtIn . getModels ()) models . set ( model . id , model ); 74return [ ...models . values ()]. filter (( model ) => ! /^(openai|anthropic)\/ / . test ( model . id )); 75}, 76async refreshModels ( context ) { 77const stored = context . stored ; 78if ( stored ?. models . length ) { 79await context . publish ({ 80update :() => { 81liveModels = stored . models . filter (( model ) => model . provider === "openrouter" ); 82}, 83}); 84} 85if ( ! context . allowNetwork || context . signal . aborted ) return ; 86if ( 87! context . force && stored ?. etag === CACHE_MARKER && stored . checkedAt && 88Date . now () - stored . checkedAt < REFRESH_INTERVAL_MS 89) return ; 90 91const response = await fetch ( CATALOG_URL , { 92headers :{ accept :"application/json" }, 93signal :AbortSignal . any ([ context . signal , AbortSignal . timeout ( 10_000 )]), 94}); 95if ( ! response . ok ) throw new Error ( `OpenRouter catalog: HTTP ${ response . status } ` ); 96const data :unknown = await response . json (); 97if ( typeof data !== "object" || data === null || ! ( "data" in data ) || ! Array . isArray ( data . data )) { 98throw new Error ( "OpenRouter catalog: invalid response" ); 99} 100const models = data . data . flatMap (( entry :unknown ) => { 101if ( typeof entry !== "object" || entry === null || typeof ( entry as OpenRouterModel ). id !== "string" || typeof ( entry as OpenRouterModel ). name !== "string" ) return []; 102const model = catalogModel ( entry as OpenRouterModel ); 103return model ?[ model ] :[]; 104}); 105if ( ! models . length ) throw new Error ( "OpenRouter catalog: no text models returned" ); 106await context . publish ({ 107persist :{ models, checkedAt :Date . now (), etag :CACHE_MARKER }, 108update :() => { liveModels = models ; }, 109}); 110}, 111}); 112 113pi . on ( "session_start" , async ( _event , ctx ) => { 114const result = await ctx . modelRegistry . refresh ({ providers :[ "openrouter" ] }); 115const error = result . errors . get ( "openrouter" ); 116if ( error ) ctx . ui . notify ( `OpenRouter catalog refresh failed: ${ error . message } ` , "warning" ); 117}); 118}