Explorar o código

Use the provider-specific model info for the OpenRouter provider (#3430)

Daniel hai 7 meses
pai
achega
9ffdc7879e

+ 25 - 0
package-lock.json

@@ -51,6 +51,7 @@
 				"puppeteer-chromium-resolver": "^23.0.0",
 				"puppeteer-core": "^23.4.0",
 				"reconnecting-eventsource": "^1.6.4",
+				"sanitize-filename": "^1.6.3",
 				"say": "^0.16.0",
 				"serialize-error": "^11.0.3",
 				"simple-git": "^3.27.0",
@@ -18264,6 +18265,15 @@
 			"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
 			"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
 		},
+		"node_modules/sanitize-filename": {
+			"version": "1.6.3",
+			"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+			"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+			"license": "WTFPL OR ISC",
+			"dependencies": {
+				"truncate-utf8-bytes": "^1.0.0"
+			}
+		},
 		"node_modules/sax": {
 			"version": "1.4.1",
 			"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
@@ -19483,6 +19493,15 @@
 				"tree-sitter-wasms": "^0.1.11"
 			}
 		},
+		"node_modules/truncate-utf8-bytes": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+			"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+			"license": "WTFPL",
+			"dependencies": {
+				"utf8-byte-length": "^1.0.1"
+			}
+		},
 		"node_modules/ts-api-utils": {
 			"version": "1.4.3",
 			"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
@@ -19930,6 +19949,12 @@
 			"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
 			"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="
 		},
+		"node_modules/utf8-byte-length": {
+			"version": "1.0.5",
+			"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+			"integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
+			"license": "(WTFPL OR MIT)"
+		},
 		"node_modules/util-deprecate": {
 			"version": "1.0.2",
 			"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

+ 1 - 0
package.json

@@ -407,6 +407,7 @@
 		"puppeteer-chromium-resolver": "^23.0.0",
 		"puppeteer-core": "^23.4.0",
 		"reconnecting-eventsource": "^1.6.4",
+		"sanitize-filename": "^1.6.3",
 		"say": "^0.16.0",
 		"serialize-error": "^11.0.3",
 		"simple-git": "^3.27.0",

+ 1 - 1
src/api/providers/__tests__/glama.test.ts

@@ -6,7 +6,7 @@ import { GlamaHandler } from "../glama"
 import { ApiHandlerOptions } from "../../../shared/api"
 
 // Mock dependencies
-jest.mock("../fetchers/cache", () => ({
+jest.mock("../fetchers/modelCache", () => ({
 	getModels: jest.fn().mockImplementation(() => {
 		return Promise.resolve({
 			"anthropic/claude-3-7-sonnet": {

+ 1 - 1
src/api/providers/__tests__/openrouter.test.ts

@@ -9,7 +9,7 @@ import { ApiHandlerOptions } from "../../../shared/api"
 // Mock dependencies
 jest.mock("openai")
 jest.mock("delay", () => jest.fn(() => Promise.resolve()))
-jest.mock("../fetchers/cache", () => ({
+jest.mock("../fetchers/modelCache", () => ({
 	getModels: jest.fn().mockImplementation(() => {
 		return Promise.resolve({
 			"anthropic/claude-3.7-sonnet": {

+ 1 - 1
src/api/providers/__tests__/requesty.test.ts

@@ -8,7 +8,7 @@ import { ApiHandlerOptions } from "../../../shared/api"
 
 jest.mock("openai")
 jest.mock("delay", () => jest.fn(() => Promise.resolve()))
-jest.mock("../fetchers/cache", () => ({
+jest.mock("../fetchers/modelCache", () => ({
 	getModels: jest.fn().mockImplementation(() => {
 		return Promise.resolve({
 			"coding/claude-3-7-sonnet": {

+ 1 - 1
src/api/providers/__tests__/unbound.test.ts

@@ -7,7 +7,7 @@ import { ApiHandlerOptions } from "../../../shared/api"
 import { UnboundHandler } from "../unbound"
 
 // Mock dependencies
-jest.mock("../fetchers/cache", () => ({
+jest.mock("../fetchers/modelCache", () => ({
 	getModels: jest.fn().mockImplementation(() => {
 		return Promise.resolve({
 			"anthropic/claude-3-5-sonnet-20241022": {

+ 25 - 0
src/api/providers/fetchers/__tests__/fixtures/openrouter-model-endpoints.json

@@ -0,0 +1,25 @@
+[
+	{
+		"scope": "https://openrouter.ai:443",
+		"method": "GET",
+		"path": "/api/v1/models/google/gemini-2.5-pro-preview/endpoints",
+		"body": "",
+		"status": 200,
+		"response": [
+			"31441d002056aa5ad5de6cfba09eb44cd983cf558aa50307224fd48d88f0c0d12137eda7bef1c435891ecc325645bf9d4794cd227137c069a7450a3f6ea3541aeacce9727170159a489e4b07a179ae738dc1a983bd860cb018631c277e3ab29720d5dea2ad528e551ef3c67c0e83e03cc3e22da9c6d2dbbb03ed2d5afa96237dbbe0d4e5e379806d0ef657edc161db2c0d863cfc7525951860c1af95425fdef6f1e177a1a24eb98a9b4ab75cb9acf4e63df938f044074a6c06dac44cda2750e3aa6e1246437d1cde032d10d0fceac4d20b07958df4a4aeec4affaa012d9b3eb5d0e3c33fdd4ad849181f1ffe53efd2b0f7f70b17431cdc7a92309228d5154e736588069b1ce7714bce6952e85c744b1cb672c175e424fda500d2300b1b3041bffe4209e02917760c1a225f6c218da952e14c3eaba01868e2fc07a68969cda1df7a9777e56ff7021bc945ab34b99e29c5222ab6214868114c9f3ebfc91c1c358cbac63aba3c18cabc99b8570923ed7b493445434205c506e4261983e7a03ac145e5e4177400cabf2a713a933092e58c0b18a4ecdf48b9d73933ec3534ee38c815670864c1a091d593757a991836ccd364e0e3e026d14b58285fe813f16ee4eaa5f285b20969d68ece56b8c01e61f98b7837320c3632314e0ce2acf4b627b7061c86ca07350aecd135c00ba71b0a08efaa5e567b2d0cbc9adc95fbb8146c53ef1fb6072b8394a59730c25e23e5e893c2a25ed4755dd70db7e0d3c42101aeda3430c89cb7df048b5a2990a64ddbac6070ceebeefc16f4f805e51cdcd44502b278439ab5eb5dbfe52eb31b84c8552f1b9aaaf32ccab7a459896918a4f4096b035bdf1a6cccc99db59ac1e0d7ec82ca95d307726386bbe8b4243aff7b14d855db2e5b0ad032c82ac88aecad09dd4eab813d6282a8dd0d947de2ecb0656ea03175e91d885361ba221b03605034261814e6c1c060c0125d58114a23c9334aa543079846052706459dce45f590e0f827bf794f3f751e24c224c06e3106cccf5c5dea93db5b0303"
+		],
+		"rawHeaders": {
+			"access-control-allow-origin": "*",
+			"cache-control": "s-maxage=300, stale-while-revalidate=600",
+			"cf-ray": "93ed496b8e0a0fb1-LAX",
+			"connection": "close",
+			"content-encoding": "br",
+			"content-type": "application/json",
+			"date": "Mon, 12 May 2025 22:17:32 GMT",
+			"server": "cloudflare",
+			"transfer-encoding": "chunked",
+			"vary": "Accept-Encoding"
+		},
+		"responseIsBinary": false
+	}
+]

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/api/providers/fetchers/__tests__/fixtures/openrouter-models.json


+ 39 - 3
src/api/providers/fetchers/__tests__/openrouter.test.ts

@@ -6,15 +6,15 @@ import { back as nockBack } from "nock"
 
 import { PROMPT_CACHING_MODELS } from "../../../../shared/api"
 
-import { getOpenRouterModels } from "../openrouter"
+import { getOpenRouterModelEndpoints, getOpenRouterModels } from "../openrouter"
 
 nockBack.fixtures = path.join(__dirname, "fixtures")
 nockBack.setMode("lockdown")
 
-describe("OpenRouter API", () => {
+describe.skip("OpenRouter API", () => {
 	describe("getOpenRouterModels", () => {
 		// This flakes in CI (probably related to Nock). Need to figure out why.
-		it.skip("fetches models and validates schema", async () => {
+		it("fetches models and validates schema", async () => {
 			const { nockDone } = await nockBack("openrouter-models.json")
 
 			const models = await getOpenRouterModels()
@@ -95,4 +95,40 @@ describe("OpenRouter API", () => {
 			nockDone()
 		})
 	})
+
+	describe("getOpenRouterModelEndpoints", () => {
+		it("fetches model endpoints and validates schema", async () => {
+			const { nockDone } = await nockBack("openrouter-model-endpoints.json")
+			const endpoints = await getOpenRouterModelEndpoints("google/gemini-2.5-pro-preview")
+
+			expect(endpoints).toEqual({
+				Google: {
+					maxTokens: 0,
+					contextWindow: 1048576,
+					supportsImages: true,
+					supportsPromptCache: true,
+					inputPrice: 1.25,
+					outputPrice: 10,
+					cacheWritesPrice: 1.625,
+					cacheReadsPrice: 0.31,
+					description: undefined,
+					thinking: false,
+				},
+				"Google AI Studio": {
+					maxTokens: 0,
+					contextWindow: 1048576,
+					supportsImages: true,
+					supportsPromptCache: true,
+					inputPrice: 1.25,
+					outputPrice: 10,
+					cacheWritesPrice: 1.625,
+					cacheReadsPrice: 0.31,
+					description: undefined,
+					thinking: false,
+				},
+			})
+
+			nockDone()
+		})
+	})
 })

+ 7 - 2
src/api/providers/fetchers/cache.ts → src/api/providers/fetchers/modelCache.ts

@@ -47,6 +47,7 @@ export const getModels = async (
 	baseUrl: string | undefined = undefined,
 ): Promise<ModelRecord> => {
 	let models = memoryCache.get<ModelRecord>(router)
+
 	if (models) {
 		// console.log(`[getModels] NodeCache hit for ${router} -> ${Object.keys(models).length}`)
 		return models
@@ -82,7 +83,9 @@ export const getModels = async (
 		try {
 			await writeModels(router, models)
 			// console.log(`[getModels] wrote ${router} models to file cache`)
-		} catch (error) {}
+		} catch (error) {
+			console.error(`[getModels] error writing ${router} models to file cache`, error)
+		}
 
 		return models
 	}
@@ -90,7 +93,9 @@ export const getModels = async (
 	try {
 		models = await readModels(router)
 		// console.log(`[getModels] read ${router} models from file cache`)
-	} catch (error) {}
+	} catch (error) {
+		console.error(`[getModels] error reading ${router} models from file cache`, error)
+	}
 
 	return models ?? {}
 }

+ 82 - 0
src/api/providers/fetchers/modelEndpointCache.ts

@@ -0,0 +1,82 @@
+import * as path from "path"
+import fs from "fs/promises"
+
+import NodeCache from "node-cache"
+import sanitize from "sanitize-filename"
+
+import { ContextProxy } from "../../../core/config/ContextProxy"
+import { getCacheDirectoryPath } from "../../../shared/storagePathManager"
+import { RouterName, ModelRecord } from "../../../shared/api"
+import { fileExistsAtPath } from "../../../utils/fs"
+
+import { getOpenRouterModelEndpoints } from "./openrouter"
+
+const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
+
+const getCacheKey = (router: RouterName, modelId: string) => sanitize(`${router}_${modelId}`)
+
+async function writeModelEndpoints(key: string, data: ModelRecord) {
+	const filename = `${key}_endpoints.json`
+	const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
+	await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data, null, 2))
+}
+
+async function readModelEndpoints(key: string): Promise<ModelRecord | undefined> {
+	const filename = `${key}_endpoints.json`
+	const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
+	const filePath = path.join(cacheDir, filename)
+	const exists = await fileExistsAtPath(filePath)
+	return exists ? JSON.parse(await fs.readFile(filePath, "utf8")) : undefined
+}
+
+export const getModelEndpoints = async ({
+	router,
+	modelId,
+	endpoint,
+}: {
+	router: RouterName
+	modelId?: string
+	endpoint?: string
+}): Promise<ModelRecord> => {
+	// OpenRouter is the only provider that supports model endpoints, but you
+	// can see how we'd extend this to other providers in the future.
+	if (router !== "openrouter" || !modelId || !endpoint) {
+		return {}
+	}
+
+	const key = getCacheKey(router, modelId)
+	let modelProviders = memoryCache.get<ModelRecord>(key)
+
+	if (modelProviders) {
+		// console.log(`[getModelProviders] NodeCache hit for ${key} -> ${Object.keys(modelProviders).length}`)
+		return modelProviders
+	}
+
+	modelProviders = await getOpenRouterModelEndpoints(modelId)
+
+	if (Object.keys(modelProviders).length > 0) {
+		// console.log(`[getModelProviders] API fetch for ${key} -> ${Object.keys(modelProviders).length}`)
+		memoryCache.set(key, modelProviders)
+
+		try {
+			await writeModelEndpoints(key, modelProviders)
+			// console.log(`[getModelProviders] wrote ${key} endpoints to file cache`)
+		} catch (error) {
+			console.error(`[getModelProviders] error writing ${key} endpoints to file cache`, error)
+		}
+
+		return modelProviders
+	}
+
+	try {
+		modelProviders = await readModelEndpoints(router)
+		// console.log(`[getModelProviders] read ${key} endpoints from file cache`)
+	} catch (error) {
+		console.error(`[getModelProviders] error reading ${key} endpoints from file cache`, error)
+	}
+
+	return modelProviders ?? {}
+}
+
+export const flushModelProviders = async (router: RouterName, modelId: string) =>
+	memoryCache.del(getCacheKey(router, modelId))

+ 168 - 65
src/api/providers/fetchers/openrouter.ts

@@ -4,42 +4,84 @@ import { z } from "zod"
 import { ApiHandlerOptions, ModelInfo, anthropicModels, COMPUTER_USE_MODELS } from "../../../shared/api"
 import { parseApiPrice } from "../../../utils/cost"
 
-// https://openrouter.ai/api/v1/models
-export const openRouterModelSchema = z.object({
-	id: z.string(),
+/**
+ * OpenRouterBaseModel
+ */
+
+const openRouterArchitectureSchema = z.object({
+	modality: z.string().nullish(),
+	tokenizer: z.string().nullish(),
+})
+
+const openRouterPricingSchema = z.object({
+	prompt: z.string().nullish(),
+	completion: z.string().nullish(),
+	input_cache_write: z.string().nullish(),
+	input_cache_read: z.string().nullish(),
+})
+
+const modelRouterBaseModelSchema = z.object({
 	name: z.string(),
 	description: z.string().optional(),
 	context_length: z.number(),
 	max_completion_tokens: z.number().nullish(),
-	architecture: z
-		.object({
-			modality: z.string().nullish(),
-			tokenizer: z.string().nullish(),
-		})
-		.optional(),
-	pricing: z
-		.object({
-			prompt: z.string().nullish(),
-			completion: z.string().nullish(),
-			input_cache_write: z.string().nullish(),
-			input_cache_read: z.string().nullish(),
-		})
-		.optional(),
-	top_provider: z
-		.object({
-			max_completion_tokens: z.number().nullish(),
-		})
-		.optional(),
+	pricing: openRouterPricingSchema.optional(),
+})
+
+export type OpenRouterBaseModel = z.infer<typeof modelRouterBaseModelSchema>
+
+/**
+ * OpenRouterModel
+ */
+
+export const openRouterModelSchema = modelRouterBaseModelSchema.extend({
+	id: z.string(),
+	architecture: openRouterArchitectureSchema.optional(),
+	top_provider: z.object({ max_completion_tokens: z.number().nullish() }).optional(),
 })
 
 export type OpenRouterModel = z.infer<typeof openRouterModelSchema>
 
+/**
+ * OpenRouterModelEndpoint
+ */
+
+export const openRouterModelEndpointSchema = modelRouterBaseModelSchema.extend({
+	provider_name: z.string(),
+})
+
+export type OpenRouterModelEndpoint = z.infer<typeof openRouterModelEndpointSchema>
+
+/**
+ * OpenRouterModelsResponse
+ */
+
 const openRouterModelsResponseSchema = z.object({
 	data: z.array(openRouterModelSchema),
 })
 
 type OpenRouterModelsResponse = z.infer<typeof openRouterModelsResponseSchema>
 
+/**
+ * OpenRouterModelEndpointsResponse
+ */
+
+const openRouterModelEndpointsResponseSchema = z.object({
+	data: z.object({
+		id: z.string(),
+		name: z.string(),
+		description: z.string().optional(),
+		architecture: openRouterArchitectureSchema.optional(),
+		endpoints: z.array(openRouterModelEndpointSchema),
+	}),
+})
+
+type OpenRouterModelEndpointsResponse = z.infer<typeof openRouterModelEndpointsResponseSchema>
+
+/**
+ * getOpenRouterModels
+ */
+
 export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<Record<string, ModelInfo>> {
 	const models: Record<string, ModelInfo> = {}
 	const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
@@ -47,54 +89,21 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
 	try {
 		const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`)
 		const result = openRouterModelsResponseSchema.safeParse(response.data)
-		const rawModels = result.success ? result.data.data : response.data.data
+		const data = result.success ? result.data.data : response.data.data
 
 		if (!result.success) {
 			console.error("OpenRouter models response is invalid", result.error.format())
 		}
 
-		for (const rawModel of rawModels) {
-			const cacheWritesPrice = rawModel.pricing?.input_cache_write
-				? parseApiPrice(rawModel.pricing?.input_cache_write)
-				: undefined
-
-			const cacheReadsPrice = rawModel.pricing?.input_cache_read
-				? parseApiPrice(rawModel.pricing?.input_cache_read)
-				: undefined
-
-			const supportsPromptCache =
-				typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
-
-			const modelInfo: ModelInfo = {
-				maxTokens: rawModel.id.startsWith("anthropic/") ? rawModel.top_provider?.max_completion_tokens : 0,
-				contextWindow: rawModel.context_length,
-				supportsImages: rawModel.architecture?.modality?.includes("image"),
-				supportsPromptCache,
-				inputPrice: parseApiPrice(rawModel.pricing?.prompt),
-				outputPrice: parseApiPrice(rawModel.pricing?.completion),
-				cacheWritesPrice,
-				cacheReadsPrice,
-				description: rawModel.description,
-				thinking: rawModel.id === "anthropic/claude-3.7-sonnet:thinking",
-			}
-
-			// The OpenRouter model definition doesn't give us any hints about
-			// computer use, so we need to set that manually.
-			if (COMPUTER_USE_MODELS.has(rawModel.id)) {
-				modelInfo.supportsComputerUse = true
-			}
-
-			// Claude 3.7 Sonnet is a "hybrid" thinking model, and the `maxTokens`
-			// values can be configured. For the non-thinking variant we want to
-			// use 8k. The `thinking` variant can be run in 64k and 128k modes,
-			// and we want to use 128k.
-			if (rawModel.id.startsWith("anthropic/claude-3.7-sonnet")) {
-				modelInfo.maxTokens = rawModel.id.includes("thinking")
-					? anthropicModels["claude-3-7-sonnet-20250219:thinking"].maxTokens
-					: anthropicModels["claude-3-7-sonnet-20250219"].maxTokens
-			}
-
-			models[rawModel.id] = modelInfo
+		for (const model of data) {
+			const { id, architecture, top_provider } = model
+
+			models[id] = parseOpenRouterModel({
+				id,
+				model,
+				modality: architecture?.modality,
+				maxTokens: id.startsWith("anthropic/") ? top_provider?.max_completion_tokens : 0,
+			})
 		}
 	} catch (error) {
 		console.error(
@@ -104,3 +113,97 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
 
 	return models
 }
+
+/**
+ * getOpenRouterModelEndpoints
+ */
+
+export async function getOpenRouterModelEndpoints(
+	modelId: string,
+	options?: ApiHandlerOptions,
+): Promise<Record<string, ModelInfo>> {
+	const models: Record<string, ModelInfo> = {}
+	const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
+
+	try {
+		const response = await axios.get<OpenRouterModelEndpointsResponse>(`${baseURL}/models/${modelId}/endpoints`)
+		const result = openRouterModelEndpointsResponseSchema.safeParse(response.data)
+		const data = result.success ? result.data.data : response.data.data
+
+		if (!result.success) {
+			console.error("OpenRouter model endpoints response is invalid", result.error.format())
+		}
+
+		const { id, architecture, endpoints } = data
+
+		for (const endpoint of endpoints) {
+			models[endpoint.provider_name] = parseOpenRouterModel({
+				id,
+				model: endpoint,
+				modality: architecture?.modality,
+				maxTokens: id.startsWith("anthropic/") ? endpoint.max_completion_tokens : 0,
+			})
+		}
+	} catch (error) {
+		console.error(
+			`Error fetching OpenRouter model endpoints: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+		)
+	}
+
+	return models
+}
+
+/**
+ * parseOpenRouterModel
+ */
+
+export const parseOpenRouterModel = ({
+	id,
+	model,
+	modality,
+	maxTokens,
+}: {
+	id: string
+	model: OpenRouterBaseModel
+	modality: string | null | undefined
+	maxTokens: number | null | undefined
+}): ModelInfo => {
+	const cacheWritesPrice = model.pricing?.input_cache_write
+		? parseApiPrice(model.pricing?.input_cache_write)
+		: undefined
+
+	const cacheReadsPrice = model.pricing?.input_cache_read ? parseApiPrice(model.pricing?.input_cache_read) : undefined
+
+	const supportsPromptCache = typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
+
+	const modelInfo: ModelInfo = {
+		maxTokens: maxTokens || 0,
+		contextWindow: model.context_length,
+		supportsImages: modality?.includes("image") ?? false,
+		supportsPromptCache,
+		inputPrice: parseApiPrice(model.pricing?.prompt),
+		outputPrice: parseApiPrice(model.pricing?.completion),
+		cacheWritesPrice,
+		cacheReadsPrice,
+		description: model.description,
+		thinking: id === "anthropic/claude-3.7-sonnet:thinking",
+	}
+
+	// The OpenRouter model definition doesn't give us any hints about
+	// computer use, so we need to set that manually.
+	if (COMPUTER_USE_MODELS.has(id)) {
+		modelInfo.supportsComputerUse = true
+	}
+
+	// Claude 3.7 Sonnet is a "hybrid" thinking model, and the `maxTokens`
+	// values can be configured. For the non-thinking variant we want to
+	// use 8k. The `thinking` variant can be run in 64k and 128k modes,
+	// and we want to use 128k.
+	if (id.startsWith("anthropic/claude-3.7-sonnet")) {
+		modelInfo.maxTokens = id.includes("thinking")
+			? anthropicModels["claude-3-7-sonnet-20250219:thinking"].maxTokens
+			: anthropicModels["claude-3-7-sonnet-20250219"].maxTokens
+	}
+
+	return modelInfo
+}

+ 21 - 3
src/api/providers/openrouter.ts

@@ -20,7 +20,8 @@ import { addCacheBreakpoints as addGeminiCacheBreakpoints } from "../transform/c
 import { getModelParams, SingleCompletionHandler } from "../index"
 import { DEFAULT_HEADERS, DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
 import { BaseProvider } from "./base-provider"
-import { getModels } from "./fetchers/cache"
+import { getModels } from "./fetchers/modelCache"
+import { getModelEndpoints } from "./fetchers/modelEndpointCache"
 
 const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
 
@@ -57,6 +58,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 	protected options: ApiHandlerOptions
 	private client: OpenAI
 	protected models: ModelRecord = {}
+	protected endpoints: ModelRecord = {}
 
 	constructor(options: ApiHandlerOptions) {
 		super()
@@ -168,13 +170,29 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 	}
 
 	public async fetchModel() {
-		this.models = await getModels("openrouter")
+		const [models, endpoints] = await Promise.all([
+			getModels("openrouter"),
+			getModelEndpoints({
+				router: "openrouter",
+				modelId: this.options.openRouterModelId,
+				endpoint: this.options.openRouterSpecificProvider,
+			}),
+		])
+
+		this.models = models
+		this.endpoints = endpoints
+
 		return this.getModel()
 	}
 
 	override getModel() {
 		const id = this.options.openRouterModelId ?? openRouterDefaultModelId
-		const info = this.models[id] ?? openRouterDefaultModelInfo
+		let info = this.models[id] ?? openRouterDefaultModelInfo
+
+		// If a specific provider is requested, use the endpoint for that provider.
+		if (this.options.openRouterSpecificProvider && this.endpoints[this.options.openRouterSpecificProvider]) {
+			info = this.endpoints[this.options.openRouterSpecificProvider]
+		}
 
 		const isDeepSeekR1 = id.startsWith("deepseek/deepseek-r1") || id === "perplexity/sonar-reasoning"
 

+ 1 - 1
src/api/providers/requesty.ts

@@ -12,7 +12,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
 import { SingleCompletionHandler } from "../"
 import { BaseProvider } from "./base-provider"
 import { DEFAULT_HEADERS } from "./constants"
-import { getModels } from "./fetchers/cache"
+import { getModels } from "./fetchers/modelCache"
 import OpenAI from "openai"
 
 // Requesty usage includes an extra field for Anthropic use cases.

+ 1 - 1
src/api/providers/router-provider.ts

@@ -2,7 +2,7 @@ import OpenAI from "openai"
 
 import { ApiHandlerOptions, RouterName, ModelRecord, ModelInfo } from "../../shared/api"
 import { BaseProvider } from "./base-provider"
-import { getModels } from "./fetchers/cache"
+import { getModels } from "./fetchers/modelCache"
 
 type RouterProviderOptions = {
 	name: RouterName

+ 1 - 1
src/core/webview/webviewMessageHandler.ts

@@ -34,7 +34,7 @@ import { TelemetrySetting } from "../../shared/TelemetrySetting"
 import { getWorkspacePath } from "../../utils/path"
 import { Mode, defaultModeSlug } from "../../shared/modes"
 import { GlobalState } from "../../schemas"
-import { getModels, flushModels } from "../../api/providers/fetchers/cache"
+import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
 import { generateSystemPrompt } from "./generateSystemPrompt"
 
 const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])

+ 30 - 5
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -34,32 +34,57 @@ import {
 } from "@roo/shared/api"
 
 import { useRouterModels } from "./useRouterModels"
+import { useOpenRouterModelProviders } from "./useOpenRouterModelProviders"
 
 export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
-	const { data: routerModels, isLoading, isError } = useRouterModels()
 	const provider = apiConfiguration?.apiProvider || "anthropic"
+	const openRouterModelId = provider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined
+
+	const routerModels = useRouterModels()
+	const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId)
 
 	const { id, info } =
-		apiConfiguration && routerModels
-			? getSelectedModel({ provider, apiConfiguration, routerModels })
+		apiConfiguration &&
+		typeof routerModels.data !== "undefined" &&
+		typeof openRouterModelProviders.data !== "undefined"
+			? getSelectedModel({
+					provider,
+					apiConfiguration,
+					routerModels: routerModels.data,
+					openRouterModelProviders: openRouterModelProviders.data,
+				})
 			: { id: anthropicDefaultModelId, info: undefined }
 
-	return { provider, id, info, isLoading, isError }
+	return {
+		provider,
+		id,
+		info,
+		isLoading: routerModels.isLoading || openRouterModelProviders.isLoading,
+		isError: routerModels.isError || openRouterModelProviders.isError,
+	}
 }
 
 function getSelectedModel({
 	provider,
 	apiConfiguration,
 	routerModels,
+	openRouterModelProviders,
 }: {
 	provider: ProviderName
 	apiConfiguration: ProviderSettings
 	routerModels: RouterModels
+	openRouterModelProviders: Record<string, ModelInfo>
 }): { id: string; info: ModelInfo } {
 	switch (provider) {
 		case "openrouter": {
 			const id = apiConfiguration.openRouterModelId ?? openRouterDefaultModelId
-			const info = routerModels.openrouter[id]
+			let info = routerModels.openrouter[id]
+			const specificProvider = apiConfiguration.openRouterSpecificProvider
+
+			if (specificProvider && openRouterModelProviders[specificProvider]) {
+				info = openRouterModelProviders[specificProvider]
+			}
+
 			return info
 				? { id, info }
 				: { id: openRouterDefaultModelId, info: routerModels.openrouter[openRouterDefaultModelId] }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio