Skip to content

Commit f93419a

Browse files
committed
Add caching layer for OpenRouter provider
1 parent 736b97a commit f93419a

File tree

8 files changed

+128
-8
lines changed

8 files changed

+128
-8
lines changed

package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@
407407
"puppeteer-chromium-resolver": "^23.0.0",
408408
"puppeteer-core": "^23.4.0",
409409
"reconnecting-eventsource": "^1.6.4",
410+
"sanitize-filename": "^1.6.3",
410411
"say": "^0.16.0",
411412
"serialize-error": "^11.0.3",
412413
"simple-git": "^3.27.0",

src/api/providers/fetchers/cache.ts renamed to src/api/providers/fetchers/modelCache.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const getModels = async (
4747
baseUrl: string | undefined = undefined,
4848
): Promise<ModelRecord> => {
4949
let models = memoryCache.get<ModelRecord>(router)
50+
5051
if (models) {
5152
// console.log(`[getModels] NodeCache hit for ${router} -> ${Object.keys(models).length}`)
5253
return models
@@ -82,15 +83,19 @@ export const getModels = async (
8283
try {
8384
await writeModels(router, models)
8485
// console.log(`[getModels] wrote ${router} models to file cache`)
85-
} catch (error) {}
86+
} catch (error) {
87+
console.error(`[getModels] error writing ${router} models to file cache`, error)
88+
}
8689

8790
return models
8891
}
8992

9093
try {
9194
models = await readModels(router)
9295
// console.log(`[getModels] read ${router} models from file cache`)
93-
} catch (error) {}
96+
} catch (error) {
97+
console.error(`[getModels] error reading ${router} models from file cache`, error)
98+
}
9499

95100
return models ?? {}
96101
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as path from "path"
2+
import fs from "fs/promises"
3+
4+
import NodeCache from "node-cache"
5+
import sanitize from "sanitize-filename"
6+
7+
import { ContextProxy } from "../../../core/config/ContextProxy"
8+
import { getCacheDirectoryPath } from "../../../shared/storagePathManager"
9+
import { RouterName, ModelRecord } from "../../../shared/api"
10+
import { fileExistsAtPath } from "../../../utils/fs"
11+
12+
import { getOpenRouterModelEndpoints } from "./openrouter"
13+
14+
const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
15+
16+
const getCacheKey = (router: RouterName, modelId: string) => sanitize(`${router}_${modelId}`)
17+
18+
async function writeModelEndpoints(key: string, data: ModelRecord) {
19+
const filename = `${key}_endpoints.json`
20+
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
21+
await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data, null, 2))
22+
}
23+
24+
async function readModelEndpoints(key: string): Promise<ModelRecord | undefined> {
25+
const filename = `${key}_endpoints.json`
26+
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
27+
const filePath = path.join(cacheDir, filename)
28+
const exists = await fileExistsAtPath(filePath)
29+
return exists ? JSON.parse(await fs.readFile(filePath, "utf8")) : undefined
30+
}
31+
32+
export const getModelEndpoints = async (router: RouterName, modelId?: string): Promise<ModelRecord> => {
33+
// OpenRouter is the only provider that supports model endpoints, but you
34+
// can see how we'd extend this to other providers in the future.
35+
if (router !== "openrouter" || !modelId) {
36+
return {}
37+
}
38+
39+
const key = getCacheKey(router, modelId)
40+
let modelProviders = memoryCache.get<ModelRecord>(key)
41+
42+
if (modelProviders) {
43+
// console.log(`[getModelProviders] NodeCache hit for ${key} -> ${Object.keys(modelProviders).length}`)
44+
return modelProviders
45+
}
46+
47+
modelProviders = await getOpenRouterModelEndpoints(modelId)
48+
49+
if (Object.keys(modelProviders).length > 0) {
50+
// console.log(`[getModelProviders] API fetch for ${key} -> ${Object.keys(modelProviders).length}`)
51+
memoryCache.set(key, modelProviders)
52+
53+
try {
54+
await writeModelEndpoints(key, modelProviders)
55+
// console.log(`[getModelProviders] wrote ${key} endpoints to file cache`)
56+
} catch (error) {
57+
console.error(`[getModelProviders] error writing ${key} endpoints to file cache`, error)
58+
}
59+
60+
return modelProviders
61+
}
62+
63+
try {
64+
modelProviders = await readModelEndpoints(router)
65+
// console.log(`[getModelProviders] read ${key} endpoints from file cache`)
66+
} catch (error) {
67+
console.error(`[getModelProviders] error reading ${key} endpoints from file cache`, error)
68+
}
69+
70+
return modelProviders ?? {}
71+
}
72+
73+
export const flushModelProviders = async (router: RouterName, modelId: string) =>
74+
memoryCache.del(getCacheKey(router, modelId))

src/api/providers/openrouter.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import { addCacheBreakpoints as addGeminiCacheBreakpoints } from "../transform/c
2020
import { getModelParams, SingleCompletionHandler } from "../index"
2121
import { DEFAULT_HEADERS, DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
2222
import { BaseProvider } from "./base-provider"
23-
import { getModels } from "./fetchers/cache"
23+
import { getModels } from "./fetchers/modelCache"
24+
import { getModelEndpoints } from "./fetchers/modelEndpointCache"
2425

2526
const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
2627

@@ -57,6 +58,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
5758
protected options: ApiHandlerOptions
5859
private client: OpenAI
5960
protected models: ModelRecord = {}
61+
protected endpoints: ModelRecord = {}
6062

6163
constructor(options: ApiHandlerOptions) {
6264
super()
@@ -168,13 +170,26 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
168170
}
169171

170172
public async fetchModel() {
171-
this.models = await getModels("openrouter")
173+
const [models, endpoints] = await Promise.all([
174+
getModels("openrouter"),
175+
getModelEndpoints("openrouter", this.options.openRouterModelId),
176+
])
177+
178+
this.models = models
179+
this.endpoints = endpoints
180+
172181
return this.getModel()
173182
}
174183

175184
override getModel() {
176185
const id = this.options.openRouterModelId ?? openRouterDefaultModelId
177-
const info = this.models[id] ?? openRouterDefaultModelInfo
186+
let info = this.models[id] ?? openRouterDefaultModelInfo
187+
188+
// If a specific provider is requested, use the endpoint for that provider.
189+
if (this.options.openRouterSpecificProvider && this.endpoints[this.options.openRouterSpecificProvider]) {
190+
info = this.endpoints[this.options.openRouterSpecificProvider]
191+
console.log(`[OpenRouterHandler] Using specific provider: ${this.options.openRouterSpecificProvider}`, info)
192+
}
178193

179194
const isDeepSeekR1 = id.startsWith("deepseek/deepseek-r1") || id === "perplexity/sonar-reasoning"
180195

src/api/providers/requesty.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
1212
import { SingleCompletionHandler } from "../"
1313
import { BaseProvider } from "./base-provider"
1414
import { DEFAULT_HEADERS } from "./constants"
15-
import { getModels } from "./fetchers/cache"
15+
import { getModels } from "./fetchers/modelCache"
1616
import OpenAI from "openai"
1717

1818
// Requesty usage includes an extra field for Anthropic use cases.

src/api/providers/router-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import OpenAI from "openai"
22

33
import { ApiHandlerOptions, RouterName, ModelRecord, ModelInfo } from "../../shared/api"
44
import { BaseProvider } from "./base-provider"
5-
import { getModels } from "./fetchers/cache"
5+
import { getModels } from "./fetchers/modelCache"
66

77
type RouterProviderOptions = {
88
name: RouterName

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { TelemetrySetting } from "../../shared/TelemetrySetting"
3434
import { getWorkspacePath } from "../../utils/path"
3535
import { Mode, defaultModeSlug } from "../../shared/modes"
3636
import { GlobalState } from "../../schemas"
37-
import { getModels, flushModels } from "../../api/providers/fetchers/cache"
37+
import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
3838
import { generateSystemPrompt } from "./generateSystemPrompt"
3939

4040
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])

0 commit comments

Comments
 (0)