Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 6 additions & 26 deletions src/api/providers/fetchers/modelCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@ import { getLiteLLMModels } from "./litellm"
import { GetModelsOptions } from "../../../shared/api"
const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })

async function writeModels(router: RouterName, data: ModelRecord) {
const filename = `${router}_models.json`
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data))
}

async function readModels(router: RouterName): Promise<ModelRecord | undefined> {
const filename = `${router}_models.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
}

/**
* Get models from the cache or fetch them from the provider and cache them.
* There are two caches:
Expand All @@ -43,15 +29,18 @@ async function readModels(router: RouterName): Promise<ModelRecord | undefined>
*/
export const getModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
const { provider } = options
let models = memoryCache.get<ModelRecord>(provider)

const cacheKey = JSON.stringify(options)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options is a small object, see the type definition of GetModelsOptions

let models = memoryCache.get<ModelRecord>(cacheKey)

if (models) {
return models
}

try {
switch (provider) {
case "openrouter":
models = await getOpenRouterModels()
models = await getOpenRouterModels(options.baseUrl, options.apiKey)
break
case "requesty":
// Requesty models endpoint requires an API key for per-user custom policies
Expand All @@ -76,17 +65,8 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
}

// Cache the fetched models (even if empty, to signify a successful fetch with no models)
memoryCache.set(provider, models)
await writeModels(provider, models).catch((err) =>
console.error(`[getModels] Error writing ${provider} models to file cache:`, err),
)
memoryCache.set(cacheKey, models)

try {
models = await readModels(provider)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cache file seems to be ineffective, because it is only read after we just wrote it, so I decided to remove it.

// console.log(`[getModels] read ${router} models from file cache`)
} catch (error) {
console.error(`[getModels] error reading ${provider} models from file cache`, error)
}
return models || {}
} catch (error) {
// Log the error and re-throw it so the caller can handle it (e.g., show a UI message).
Expand Down
8 changes: 5 additions & 3 deletions src/api/providers/fetchers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ type OpenRouterModelEndpointsResponse = z.infer<typeof openRouterModelEndpointsR
* getOpenRouterModels
*/

export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<Record<string, ModelInfo>> {
export async function getOpenRouterModels(baseUrl?: string, apiKey?: string): Promise<Record<string, ModelInfo>> {
const models: Record<string, ModelInfo> = {}
const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
const baseURL = baseUrl || "https://openrouter.ai/api/v1"

try {
const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`)
const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`, {
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
})
const result = openRouterModelsResponseSchema.safeParse(response.data)
const data = result.success ? result.data.data : response.data.data

Expand Down
2 changes: 1 addition & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ export class ClineProvider
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://* https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<script nonce="${nonce}">
Expand Down
2 changes: 1 addition & 1 deletion src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2136,7 +2136,7 @@ describe("ClineProvider - Router Models", () => {
await messageHandler({ type: "requestRouterModels" })

// Verify getModels was called for each provider with correct options
expect(getModels).toHaveBeenCalledWith({ provider: "openrouter" })
expect(getModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key" })
expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
expect(getModels).toHaveBeenCalledWith({ provider: "glama" })
expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
Expand Down
2 changes: 1 addition & 1 deletion src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
})

// Verify getModels was called for each provider
expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
Expand Down
8 changes: 7 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,14 @@ export const webviewMessageHandler = async (
}
}

const openRouterApiKey = apiConfiguration.openRouterApiKey || message?.values?.openRouterApiKey
const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || message?.values?.openRouterBaseUrl

const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
{ key: "openrouter", options: { provider: "openrouter" } },
{
key: "openrouter",
options: { provider: "openrouter", apiKey: openRouterApiKey, baseUrl: openRouterBaseUrl },
},
{ key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey } },
{ key: "glama", options: { provider: "glama" } },
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
Expand Down
2 changes: 1 addition & 1 deletion src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const getModelMaxOutputTokens = ({
// GetModelsOptions

export type GetModelsOptions =
| { provider: "openrouter" }
| { provider: "openrouter"; apiKey?: string; baseUrl?: string }
| { provider: "glama" }
| { provider: "requesty"; apiKey?: string }
| { provider: "unbound"; apiKey?: string }
Expand Down
5 changes: 4 additions & 1 deletion webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ const ApiOptions = ({
info: selectedModelInfo,
} = useSelectedModel(apiConfiguration)

const { data: routerModels, refetch: refetchRouterModels } = useRouterModels()
const { data: routerModels, refetch: refetchRouterModels } = useRouterModels({
openRouterBaseUrl: apiConfiguration?.openRouterBaseUrl,
openRouterApiKey: apiConfiguration?.openRouterApiKey,
})

// Update `apiModelId` whenever `selectedModelId` changes.
useEffect(() => {
Expand Down
19 changes: 12 additions & 7 deletions webview-ui/src/components/settings/providers/OpenRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,18 @@ export const OpenRouter = ({
[setApiConfigurationField],
)

const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, {
enabled:
!!apiConfiguration?.openRouterModelId &&
routerModels?.openrouter &&
Object.keys(routerModels.openrouter).length > 1 &&
apiConfiguration.openRouterModelId in routerModels.openrouter,
})
const { data: openRouterModelProviders } = useOpenRouterModelProviders(
apiConfiguration?.openRouterModelId,
apiConfiguration?.openRouterBaseUrl,
apiConfiguration?.apiKey,
{
enabled:
!!apiConfiguration?.openRouterModelId &&
routerModels?.openrouter &&
Object.keys(routerModels.openrouter).length > 1 &&
apiConfiguration.openRouterModelId in routerModels.openrouter,
},
)

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ type OpenRouterModelProvider = ModelInfo & {
label: string
}

async function getOpenRouterProvidersForModel(modelId: string) {
async function getOpenRouterProvidersForModel(modelId: string, baseUrl?: string, apiKey?: string) {
const models: Record<string, OpenRouterModelProvider> = {}

try {
const response = await axios.get(`https://openrouter.ai/api/v1/models/${modelId}/endpoints`)
const response = await axios.get(
`${baseUrl?.trim() || "https://openrouter.ai/api/v1"}/models/${modelId}/endpoints`,
apiKey ? { headers: { Authorization: `Bearer ${apiKey}` } } : undefined,
)
const result = openRouterEndpointsSchema.safeParse(response.data)

if (!result.success) {
Expand Down Expand Up @@ -112,9 +115,14 @@ type UseOpenRouterModelProvidersOptions = Omit<
"queryKey" | "queryFn"
>

export const useOpenRouterModelProviders = (modelId?: string, options?: UseOpenRouterModelProvidersOptions) =>
export const useOpenRouterModelProviders = (
modelId?: string,
baseUrl?: string,
apiKey?: string,
options?: UseOpenRouterModelProvidersOptions,
) =>
useQuery<Record<string, OpenRouterModelProvider>>({
queryKey: ["openrouter-model-providers", modelId],
queryFn: () => (modelId ? getOpenRouterProvidersForModel(modelId) : {}),
queryKey: ["openrouter-model-providers", modelId, baseUrl, apiKey],
queryFn: () => (modelId ? getOpenRouterProvidersForModel(modelId, baseUrl, apiKey) : {}),
...options,
})
8 changes: 7 additions & 1 deletion webview-ui/src/components/ui/hooks/useRouterModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ const getRouterModels = async () =>
vscode.postMessage({ type: "requestRouterModels" })
})

export const useRouterModels = () => useQuery({ queryKey: ["routerModels"], queryFn: getRouterModels })
type RouterModelsQueryKey = {
openRouterBaseUrl?: string
openRouterApiKey?: string
}

export const useRouterModels = (queryKey: RouterModelsQueryKey) =>
useQuery({ queryKey: ["routerModels", queryKey], queryFn: getRouterModels })
11 changes: 9 additions & 2 deletions webview-ui/src/components/ui/hooks/useSelectedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
const provider = apiConfiguration?.apiProvider || "anthropic"
const openRouterModelId = provider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined

const routerModels = useRouterModels()
const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId)
const routerModels = useRouterModels({
openRouterBaseUrl: apiConfiguration?.openRouterBaseUrl,
openRouterApiKey: apiConfiguration?.apiKey,
})
const openRouterModelProviders = useOpenRouterModelProviders(
openRouterModelId,
apiConfiguration?.openRouterBaseUrl,
apiConfiguration?.apiKey,
)

const { id, info } =
apiConfiguration &&
Expand Down
Loading