@@ -4,97 +4,106 @@ import { z } from "zod"
44import { ApiHandlerOptions , ModelInfo , anthropicModels , COMPUTER_USE_MODELS } from "../../../shared/api"
55import { parseApiPrice } from "../../../utils/cost"
66
7- // https://openrouter.ai/api/v1/models
8- export const openRouterModelSchema = z . object ( {
9- id : z . string ( ) ,
7+ /**
8+ * OpenRouterBaseModel
9+ */
10+
11+ const openRouterArchitectureSchema = z . object ( {
12+ modality : z . string ( ) . nullish ( ) ,
13+ tokenizer : z . string ( ) . nullish ( ) ,
14+ } )
15+
16+ const openRouterPricingSchema = z . object ( {
17+ prompt : z . string ( ) . nullish ( ) ,
18+ completion : z . string ( ) . nullish ( ) ,
19+ input_cache_write : z . string ( ) . nullish ( ) ,
20+ input_cache_read : z . string ( ) . nullish ( ) ,
21+ } )
22+
23+ const modelRouterBaseModelSchema = z . object ( {
1024 name : z . string ( ) ,
1125 description : z . string ( ) . optional ( ) ,
1226 context_length : z . number ( ) ,
1327 max_completion_tokens : z . number ( ) . nullish ( ) ,
14- architecture : z
15- . object ( {
16- modality : z . string ( ) . nullish ( ) ,
17- tokenizer : z . string ( ) . nullish ( ) ,
18- } )
19- . optional ( ) ,
20- pricing : z
21- . object ( {
22- prompt : z . string ( ) . nullish ( ) ,
23- completion : z . string ( ) . nullish ( ) ,
24- input_cache_write : z . string ( ) . nullish ( ) ,
25- input_cache_read : z . string ( ) . nullish ( ) ,
26- } )
27- . optional ( ) ,
28- top_provider : z
29- . object ( {
30- max_completion_tokens : z . number ( ) . nullish ( ) ,
31- } )
32- . optional ( ) ,
28+ pricing : openRouterPricingSchema . optional ( ) ,
29+ } )
30+
31+ export type OpenRouterBaseModel = z . infer < typeof modelRouterBaseModelSchema >
32+
33+ /**
34+ * OpenRouterModel
35+ */
36+
37+ export const openRouterModelSchema = modelRouterBaseModelSchema . extend ( {
38+ id : z . string ( ) ,
39+ architecture : openRouterArchitectureSchema . optional ( ) ,
40+ top_provider : z . object ( { max_completion_tokens : z . number ( ) . nullish ( ) } ) . optional ( ) ,
3341} )
3442
3543export type OpenRouterModel = z . infer < typeof openRouterModelSchema >
3644
45+ /**
46+ * OpenRouterModelEndpoint
47+ */
48+
49+ export const openRouterModelEndpointSchema = modelRouterBaseModelSchema . extend ( {
50+ provider_name : z . string ( ) ,
51+ } )
52+
53+ export type OpenRouterModelEndpoint = z . infer < typeof openRouterModelEndpointSchema >
54+
55+ /**
56+ * OpenRouterModelsResponse
57+ */
58+
3759const openRouterModelsResponseSchema = z . object ( {
3860 data : z . array ( openRouterModelSchema ) ,
3961} )
4062
4163type OpenRouterModelsResponse = z . infer < typeof openRouterModelsResponseSchema >
4264
65+ /**
66+ * OpenRouterModelEndpointsResponse
67+ */
68+
69+ const openRouterModelEndpointsResponseSchema = z . object ( {
70+ data : z . object ( {
71+ id : z . string ( ) ,
72+ name : z . string ( ) ,
73+ description : z . string ( ) . optional ( ) ,
74+ architecture : openRouterArchitectureSchema . optional ( ) ,
75+ endpoints : z . array ( openRouterModelEndpointSchema ) ,
76+ } ) ,
77+ } )
78+
79+ type OpenRouterModelEndpointsResponse = z . infer < typeof openRouterModelEndpointsResponseSchema >
80+
81+ /**
82+ * getOpenRouterModels
83+ */
84+
4385export async function getOpenRouterModels ( options ?: ApiHandlerOptions ) : Promise < Record < string , ModelInfo > > {
4486 const models : Record < string , ModelInfo > = { }
4587 const baseURL = options ?. openRouterBaseUrl || "https://openrouter.ai/api/v1"
4688
4789 try {
4890 const response = await axios . get < OpenRouterModelsResponse > ( `${ baseURL } /models` )
4991 const result = openRouterModelsResponseSchema . safeParse ( response . data )
50- const rawModels = result . success ? result . data . data : response . data . data
92+ const data = result . success ? result . data . data : response . data . data
5193
5294 if ( ! result . success ) {
5395 console . error ( "OpenRouter models response is invalid" , result . error . format ( ) )
5496 }
5597
56- for ( const rawModel of rawModels ) {
57- const cacheWritesPrice = rawModel . pricing ?. input_cache_write
58- ? parseApiPrice ( rawModel . pricing ?. input_cache_write )
59- : undefined
60-
61- const cacheReadsPrice = rawModel . pricing ?. input_cache_read
62- ? parseApiPrice ( rawModel . pricing ?. input_cache_read )
63- : undefined
64-
65- const supportsPromptCache =
66- typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
67-
68- const modelInfo : ModelInfo = {
69- maxTokens : rawModel . id . startsWith ( "anthropic/" ) ? rawModel . top_provider ?. max_completion_tokens : 0 ,
70- contextWindow : rawModel . context_length ,
71- supportsImages : rawModel . architecture ?. modality ?. includes ( "image" ) ,
72- supportsPromptCache,
73- inputPrice : parseApiPrice ( rawModel . pricing ?. prompt ) ,
74- outputPrice : parseApiPrice ( rawModel . pricing ?. completion ) ,
75- cacheWritesPrice,
76- cacheReadsPrice,
77- description : rawModel . description ,
78- thinking : rawModel . id === "anthropic/claude-3.7-sonnet:thinking" ,
79- }
80-
81- // The OpenRouter model definition doesn't give us any hints about
82- // computer use, so we need to set that manually.
83- if ( COMPUTER_USE_MODELS . has ( rawModel . id ) ) {
84- modelInfo . supportsComputerUse = true
85- }
86-
87- // Claude 3.7 Sonnet is a "hybrid" thinking model, and the `maxTokens`
88- // values can be configured. For the non-thinking variant we want to
89- // use 8k. The `thinking` variant can be run in 64k and 128k modes,
90- // and we want to use 128k.
91- if ( rawModel . id . startsWith ( "anthropic/claude-3.7-sonnet" ) ) {
92- modelInfo . maxTokens = rawModel . id . includes ( "thinking" )
93- ? anthropicModels [ "claude-3-7-sonnet-20250219:thinking" ] . maxTokens
94- : anthropicModels [ "claude-3-7-sonnet-20250219" ] . maxTokens
95- }
96-
97- models [ rawModel . id ] = modelInfo
98+ for ( const model of data ) {
99+ const { id, architecture, top_provider } = model
100+
101+ models [ id ] = parseOpenRouterModel ( {
102+ id,
103+ model,
104+ modality : architecture ?. modality ,
105+ maxTokens : id . startsWith ( "anthropic/" ) ? top_provider ?. max_completion_tokens : 0 ,
106+ } )
98107 }
99108 } catch ( error ) {
100109 console . error (
@@ -104,3 +113,97 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
104113
105114 return models
106115}
116+
117+ /**
118+ * getOpenRouterModelEndpoints
119+ */
120+
121+ export async function getOpenRouterModelEndpoints (
122+ modelId : string ,
123+ options ?: ApiHandlerOptions ,
124+ ) : Promise < Record < string , ModelInfo > > {
125+ const models : Record < string , ModelInfo > = { }
126+ const baseURL = options ?. openRouterBaseUrl || "https://openrouter.ai/api/v1"
127+
128+ try {
129+ const response = await axios . get < OpenRouterModelEndpointsResponse > ( `${ baseURL } /models/${ modelId } /endpoints` )
130+ const result = openRouterModelEndpointsResponseSchema . safeParse ( response . data )
131+ const data = result . success ? result . data . data : response . data . data
132+
133+ if ( ! result . success ) {
134+ console . error ( "OpenRouter model endpoints response is invalid" , result . error . format ( ) )
135+ }
136+
137+ const { id, architecture, endpoints } = data
138+
139+ for ( const endpoint of endpoints ) {
140+ models [ endpoint . provider_name ] = parseOpenRouterModel ( {
141+ id,
142+ model : endpoint ,
143+ modality : architecture ?. modality ,
144+ maxTokens : id . startsWith ( "anthropic/" ) ? endpoint . max_completion_tokens : 0 ,
145+ } )
146+ }
147+ } catch ( error ) {
148+ console . error (
149+ `Error fetching OpenRouter model endpoints: ${ JSON . stringify ( error , Object . getOwnPropertyNames ( error ) , 2 ) } ` ,
150+ )
151+ }
152+
153+ return models
154+ }
155+
156+ /**
157+ * parseOpenRouterModel
158+ */
159+
160+ export const parseOpenRouterModel = ( {
161+ id,
162+ model,
163+ modality,
164+ maxTokens,
165+ } : {
166+ id : string
167+ model : OpenRouterBaseModel
168+ modality : string | null | undefined
169+ maxTokens : number | null | undefined
170+ } ) : ModelInfo => {
171+ const cacheWritesPrice = model . pricing ?. input_cache_write
172+ ? parseApiPrice ( model . pricing ?. input_cache_write )
173+ : undefined
174+
175+ const cacheReadsPrice = model . pricing ?. input_cache_read ? parseApiPrice ( model . pricing ?. input_cache_read ) : undefined
176+
177+ const supportsPromptCache = typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
178+
179+ const modelInfo : ModelInfo = {
180+ maxTokens : maxTokens || 0 ,
181+ contextWindow : model . context_length ,
182+ supportsImages : modality ?. includes ( "image" ) ?? false ,
183+ supportsPromptCache,
184+ inputPrice : parseApiPrice ( model . pricing ?. prompt ) ,
185+ outputPrice : parseApiPrice ( model . pricing ?. completion ) ,
186+ cacheWritesPrice,
187+ cacheReadsPrice,
188+ description : model . description ,
189+ thinking : id === "anthropic/claude-3.7-sonnet:thinking" ,
190+ }
191+
192+ // The OpenRouter model definition doesn't give us any hints about
193+ // computer use, so we need to set that manually.
194+ if ( COMPUTER_USE_MODELS . has ( id ) ) {
195+ modelInfo . supportsComputerUse = true
196+ }
197+
198+ // Claude 3.7 Sonnet is a "hybrid" thinking model, and the `maxTokens`
199+ // values can be configured. For the non-thinking variant we want to
200+ // use 8k. The `thinking` variant can be run in 64k and 128k modes,
201+ // and we want to use 128k.
202+ if ( id . startsWith ( "anthropic/claude-3.7-sonnet" ) ) {
203+ modelInfo . maxTokens = id . includes ( "thinking" )
204+ ? anthropicModels [ "claude-3-7-sonnet-20250219:thinking" ] . maxTokens
205+ : anthropicModels [ "claude-3-7-sonnet-20250219" ] . maxTokens
206+ }
207+
208+ return modelInfo
209+ }
0 commit comments