Skip to content

Commit 8e66607

Browse files
authored
feat: update OpenRouter API to support input/output modalities and filter image generation models (#7492)
1 parent bea0684 commit 8e66607

File tree

4 files changed

+88
-13
lines changed

4 files changed

+88
-13
lines changed

apps/web-roo-code/src/lib/hooks/use-open-router-models.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export const openRouterModelSchema = z.object({
2222
.optional(),
2323
architecture: z
2424
.object({
25-
modality: z.string(),
25+
input_modalities: z.array(z.string()).nullish(),
26+
output_modalities: z.array(z.string()).nullish(),
2627
})
2728
.optional(),
2829
})
@@ -47,6 +48,10 @@ export const getOpenRouterModels = async (): Promise<OpenRouterModelRecord> => {
4748
}
4849

4950
return result.data.data
51+
.filter((rawModel) => {
52+
// Skip image generation models (models that output images)
53+
return !rawModel.architecture?.output_modalities?.includes("image")
54+
})
5055
.sort((a, b) => a.name.localeCompare(b.name))
5156
.map((rawModel) => ({
5257
...rawModel,
@@ -57,7 +62,7 @@ export const getOpenRouterModels = async (): Promise<OpenRouterModelRecord> => {
5762
outputPrice: parsePrice(rawModel.pricing?.completion),
5863
description: rawModel.description,
5964
supportsPromptCache: false,
60-
supportsImages: false,
65+
supportsImages: rawModel.architecture?.input_modalities?.includes("image") ?? false,
6166
supportsThinking: false,
6267
tiers: [],
6368
},

src/api/providers/fetchers/__tests__/openrouter.spec.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ describe("OpenRouter API", () => {
280280
const result = parseOpenRouterModel({
281281
id: "openrouter/horizon-alpha",
282282
model: mockModel,
283-
modality: "text",
283+
inputModality: ["text"],
284+
outputModality: ["text"],
284285
maxTokens: 128000,
285286
})
286287

@@ -303,7 +304,8 @@ describe("OpenRouter API", () => {
303304
const result = parseOpenRouterModel({
304305
id: "openrouter/horizon-beta",
305306
model: mockModel,
306-
modality: "text",
307+
inputModality: ["text"],
308+
outputModality: ["text"],
307309
maxTokens: 128000,
308310
})
309311

@@ -326,12 +328,59 @@ describe("OpenRouter API", () => {
326328
const result = parseOpenRouterModel({
327329
id: "openrouter/other-model",
328330
model: mockModel,
329-
modality: "text",
331+
inputModality: ["text"],
332+
outputModality: ["text"],
330333
maxTokens: 64000,
331334
})
332335

333336
expect(result.maxTokens).toBe(64000)
334337
expect(result.contextWindow).toBe(128000)
335338
})
339+
340+
it("filters out image generation models", () => {
341+
const mockImageModel = {
342+
name: "Image Model",
343+
description: "Test image generation model",
344+
context_length: 128000,
345+
max_completion_tokens: 64000,
346+
pricing: {
347+
prompt: "0.000003",
348+
completion: "0.000015",
349+
},
350+
}
351+
352+
const mockTextModel = {
353+
name: "Text Model",
354+
description: "Test text generation model",
355+
context_length: 128000,
356+
max_completion_tokens: 64000,
357+
pricing: {
358+
prompt: "0.000003",
359+
completion: "0.000015",
360+
},
361+
}
362+
363+
// Model with image output should be filtered out - we only test parseOpenRouterModel
364+
// since the filtering happens in getOpenRouterModels/getOpenRouterModelEndpoints
365+
const textResult = parseOpenRouterModel({
366+
id: "test/text-model",
367+
model: mockTextModel,
368+
inputModality: ["text"],
369+
outputModality: ["text"],
370+
maxTokens: 64000,
371+
})
372+
373+
const imageResult = parseOpenRouterModel({
374+
id: "test/image-model",
375+
model: mockImageModel,
376+
inputModality: ["text"],
377+
outputModality: ["image"],
378+
maxTokens: 64000,
379+
})
380+
381+
// Both should parse successfully (filtering happens at a higher level)
382+
expect(textResult.maxTokens).toBe(64000)
383+
expect(imageResult.maxTokens).toBe(64000)
384+
})
336385
})
337386
})

src/api/providers/fetchers/openrouter.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { parseApiPrice } from "../../../shared/cost"
1818
*/
1919

2020
const openRouterArchitectureSchema = z.object({
21-
modality: z.string().nullish(),
21+
input_modalities: z.array(z.string()).nullish(),
22+
output_modalities: z.array(z.string()).nullish(),
2223
tokenizer: z.string().nullish(),
2324
})
2425

@@ -110,10 +111,16 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
110111
for (const model of data) {
111112
const { id, architecture, top_provider, supported_parameters = [] } = model
112113

114+
// Skip image generation models (models that output images)
115+
if (architecture?.output_modalities?.includes("image")) {
116+
continue
117+
}
118+
113119
models[id] = parseOpenRouterModel({
114120
id,
115121
model,
116-
modality: architecture?.modality,
122+
inputModality: architecture?.input_modalities,
123+
outputModality: architecture?.output_modalities,
117124
maxTokens: top_provider?.max_completion_tokens,
118125
supportedParameters: supported_parameters,
119126
})
@@ -149,11 +156,17 @@ export async function getOpenRouterModelEndpoints(
149156

150157
const { id, architecture, endpoints } = data
151158

159+
// Skip image generation models (models that output images)
160+
if (architecture?.output_modalities?.includes("image")) {
161+
return models
162+
}
163+
152164
for (const endpoint of endpoints) {
153165
models[endpoint.tag ?? endpoint.provider_name] = parseOpenRouterModel({
154166
id,
155167
model: endpoint,
156-
modality: architecture?.modality,
168+
inputModality: architecture?.input_modalities,
169+
outputModality: architecture?.output_modalities,
157170
maxTokens: endpoint.max_completion_tokens,
158171
})
159172
}
@@ -173,13 +186,15 @@ export async function getOpenRouterModelEndpoints(
173186
export const parseOpenRouterModel = ({
174187
id,
175188
model,
176-
modality,
189+
inputModality,
190+
outputModality,
177191
maxTokens,
178192
supportedParameters,
179193
}: {
180194
id: string
181195
model: OpenRouterBaseModel
182-
modality: string | null | undefined
196+
inputModality: string[] | null | undefined
197+
outputModality: string[] | null | undefined
183198
maxTokens: number | null | undefined
184199
supportedParameters?: string[]
185200
}): ModelInfo => {
@@ -194,7 +209,7 @@ export const parseOpenRouterModel = ({
194209
const modelInfo: ModelInfo = {
195210
maxTokens: maxTokens || Math.ceil(model.context_length * 0.2),
196211
contextWindow: model.context_length,
197-
supportsImages: modality?.includes("image") ?? false,
212+
supportsImages: inputModality?.includes("image") ?? false,
198213
supportsPromptCache,
199214
inputPrice: parseApiPrice(model.pricing?.prompt),
200215
outputPrice: parseApiPrice(model.pricing?.completion),

webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const openRouterEndpointsSchema = z.object({
1515
description: z.string().optional(),
1616
architecture: z
1717
.object({
18-
modality: z.string().nullish(),
18+
input_modalities: z.array(z.string()).nullish(),
19+
output_modalities: z.array(z.string()).nullish(),
1920
tokenizer: z.string().nullish(),
2021
})
2122
.nullish(),
@@ -56,6 +57,11 @@ async function getOpenRouterProvidersForModel(modelId: string) {
5657

5758
const { description, architecture, endpoints } = result.data.data
5859

60+
// Skip image generation models (models that output images)
61+
if (architecture?.output_modalities?.includes("image")) {
62+
return models
63+
}
64+
5965
for (const endpoint of endpoints) {
6066
const providerName = endpoint.tag ?? endpoint.name
6167
const inputPrice = parseApiPrice(endpoint.pricing?.prompt)
@@ -66,7 +72,7 @@ async function getOpenRouterProvidersForModel(modelId: string) {
6672
const modelInfo: OpenRouterModelProvider = {
6773
maxTokens: endpoint.max_completion_tokens || endpoint.context_length,
6874
contextWindow: endpoint.context_length,
69-
supportsImages: architecture?.modality?.includes("image"),
75+
supportsImages: architecture?.input_modalities?.includes("image") ?? false,
7076
supportsPromptCache: typeof cacheReadsPrice !== "undefined",
7177
cacheReadsPrice,
7278
cacheWritesPrice,

0 commit comments

Comments
 (0)