mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Use the provider-specific model info for the OpenRouter provider (#3430)
This commit is contained in:
parent
79d0e5039b
commit
9ffdc7879e
17 changed files with 407 additions and 87 deletions
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
src/api/providers/fetchers/modelEndpointCache.ts
Normal file
82
src/api/providers/fetchers/modelEndpointCache.ts
Normal 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))
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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] }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue