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

This commit is contained in:
Daniel 2025-05-12 20:23:32 -05:00 committed by GitHub
parent 79d0e5039b
commit 9ffdc7879e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 407 additions and 87 deletions

25
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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
}
]

File diff suppressed because one or more lines are too long

View file

@ -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()
})
})
})

View file

@ -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 ?? {}
}

View file

@ -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))

View file

@ -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
for (const model of data) {
const { id, architecture, top_provider } = model
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
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
}

View file

@ -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"

View file

@ -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.

View file

@ -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

View file

@ -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"])

View file

@ -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] }