diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index adb54fcb03..a60044d2d5 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -26,9 +26,6 @@ import { getRooModels } from "./roo" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) -// Coalesce concurrent fetches per provider within this extension host -const inFlightModelFetches = new Map>() - async function writeModels(router: RouterName, data: ModelRecord) { const filename = `${router}_models.json` const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath) @@ -63,160 +60,75 @@ export const getModels = async (options: GetModelsOptions): Promise return cached } - // 2) Try file cache snapshot (Option A), then kick off background refresh + // 2) Try file cache snapshot try { const file = await readModels(provider) if (file && Object.keys(file).length > 0) { memoryCache.set(provider, file) - - // Start background refresh if not already in-flight (do not await) - if (!inFlightModelFetches.has(provider)) { - const signal = AbortSignal.timeout(30_000) - const bgPromise = (async (): Promise => { - let models: ModelRecord = {} - switch (provider) { - case "openrouter": - models = await getOpenRouterModels(undefined, signal) - break - case "requesty": - models = await getRequestyModels(options.baseUrl, options.apiKey, signal) - break - case "glama": - models = await getGlamaModels(signal) - break - case "unbound": - models = await getUnboundModels(options.apiKey, signal) - break - case "litellm": - models = await getLiteLLMModels(options.apiKey as string, options.baseUrl as string, signal) - break - case "ollama": - models = await getOllamaModels(options.baseUrl, options.apiKey, signal) - break - case "lmstudio": - models = await getLMStudioModels(options.baseUrl, signal) - break - case "deepinfra": - models = await getDeepInfraModels(options.apiKey, options.baseUrl, signal) - break - case "io-intelligence": - models = await getIOIntelligenceModels(options.apiKey, signal) - break - case "vercel-ai-gateway": - models = await getVercelAiGatewayModels(undefined, signal) - break - case "huggingface": - models = await getHuggingFaceModels(signal) - break - case "roo": { - const rooBaseUrl = - options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy" - models = await getRooModels(rooBaseUrl, options.apiKey, signal) - break - } - default: - throw new Error(`Unknown provider: ${provider}`) - } - - memoryCache.set(provider, models) - await writeModels(provider, models).catch((err) => { - console.error( - `[modelCache] Error writing ${provider} to file cache during background refresh:`, - err instanceof Error ? err.message : String(err), - ) - }) - return models || {} - })() - - inFlightModelFetches.set(provider, bgPromise) - Promise.resolve(bgPromise) - .catch((err) => { - console.error( - `[modelCache] Background refresh failed for ${provider}:`, - err instanceof Error ? err.message : String(err), - ) - }) - .finally(() => inFlightModelFetches.delete(provider)) - } - return file } } catch { - // ignore file read errors; fall through to network/coalesce path + // ignore file read errors; fall through to network fetch } - // 3) Coalesce concurrent fetches - const existing = inFlightModelFetches.get(provider) - if (existing) { - return existing - } - - // 4) Network fetch wrapped as a single in-flight promise for this provider + // 3) Network fetch const signal = AbortSignal.timeout(30_000) - const fetchPromise = (async (): Promise => { - let models: ModelRecord = {} - switch (provider) { - case "openrouter": - models = await getOpenRouterModels(undefined, signal) - break - case "requesty": - models = await getRequestyModels(options.baseUrl, options.apiKey, signal) - break - case "glama": - models = await getGlamaModels(signal) - break - case "unbound": - models = await getUnboundModels(options.apiKey, signal) - break - case "litellm": - models = await getLiteLLMModels(options.apiKey as string, options.baseUrl as string, signal) - break - case "ollama": - models = await getOllamaModels(options.baseUrl, options.apiKey, signal) - break - case "lmstudio": - models = await getLMStudioModels(options.baseUrl, signal) - break - case "deepinfra": - models = await getDeepInfraModels(options.apiKey, options.baseUrl, signal) - break - case "io-intelligence": - models = await getIOIntelligenceModels(options.apiKey, signal) - break - case "vercel-ai-gateway": - models = await getVercelAiGatewayModels(undefined, signal) - break - case "huggingface": - models = await getHuggingFaceModels(signal) - break - case "roo": { - const rooBaseUrl = - options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy" - models = await getRooModels(rooBaseUrl, options.apiKey, signal) - break - } - default: { - throw new Error(`Unknown provider: ${provider}`) - } + let models: ModelRecord = {} + + switch (provider) { + case "openrouter": + models = await getOpenRouterModels(undefined, signal) + break + case "requesty": + models = await getRequestyModels(options.baseUrl, options.apiKey, signal) + break + case "glama": + models = await getGlamaModels(signal) + break + case "unbound": + models = await getUnboundModels(options.apiKey, signal) + break + case "litellm": + models = await getLiteLLMModels(options.apiKey as string, options.baseUrl as string, signal) + break + case "ollama": + models = await getOllamaModels(options.baseUrl, options.apiKey, signal) + break + case "lmstudio": + models = await getLMStudioModels(options.baseUrl, signal) + break + case "deepinfra": + models = await getDeepInfraModels(options.apiKey, options.baseUrl, signal) + break + case "io-intelligence": + models = await getIOIntelligenceModels(options.apiKey, signal) + break + case "vercel-ai-gateway": + models = await getVercelAiGatewayModels(undefined, signal) + break + case "huggingface": + models = await getHuggingFaceModels(signal) + break + case "roo": { + const rooBaseUrl = options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy" + models = await getRooModels(rooBaseUrl, options.apiKey, signal) + break + } + default: { + throw new Error(`Unknown provider: ${provider}`) } - memoryCache.set(provider, models) - - await writeModels(provider, models).catch((err) => { - console.error( - `[modelCache] Error writing ${provider} to file cache after network fetch:`, - err instanceof Error ? err.message : String(err), - ) - }) - - return models || {} - })() - - inFlightModelFetches.set(provider, fetchPromise) - try { - return await fetchPromise - } finally { - inFlightModelFetches.delete(provider) } + + memoryCache.set(provider, models) + + await writeModels(provider, models).catch((err) => { + console.error( + `[modelCache] Error writing ${provider} to file cache after network fetch:`, + err instanceof Error ? err.message : String(err), + ) + }) + + return models || {} } /** diff --git a/src/api/providers/fetchers/modelEndpointCache.ts b/src/api/providers/fetchers/modelEndpointCache.ts index 9a9018e4d9..49322a6a7d 100644 --- a/src/api/providers/fetchers/modelEndpointCache.ts +++ b/src/api/providers/fetchers/modelEndpointCache.ts @@ -14,9 +14,6 @@ import { getOpenRouterModelEndpoints } from "./openrouter" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) -// Coalesce concurrent endpoint fetches per (router,modelId) -const inFlightEndpointFetches = new Map>() - const getCacheKey = (router: RouterName, modelId: string) => sanitize(`${router}_${modelId}`) async function writeModelEndpoints(key: string, data: ModelRecord) { @@ -53,107 +50,47 @@ export const getModelEndpoints = async ({ // 1) Try memory cache const cached = memoryCache.get(key) if (cached) { - // Using console.log for cache layer logging (no provider access in utility functions) - console.log(`[endpointCache] cache_hit: ${key} (${Object.keys(cached).length} endpoints)`) return cached } - // 2) Try file cache snapshot (Option A), then kick off background refresh + // 2) Try file cache snapshot try { const file = await readModelEndpoints(key) if (file && Object.keys(file).length > 0) { - // Using console.log for cache layer logging (no provider access in utility functions) - console.log(`[endpointCache] file_hit: ${key} (${Object.keys(file).length} endpoints, bg_refresh queued)`) - // Populate memory cache immediately memoryCache.set(key, file) - - // Start background refresh if not already in-flight (do not await) - if (!inFlightEndpointFetches.has(key)) { - const signal = AbortSignal.timeout(30_000) - const bgPromise = (async (): Promise => { - const modelProviders = await getOpenRouterModelEndpoints(modelId, undefined, signal) - if (Object.keys(modelProviders).length > 0) { - console.log( - `[endpointCache] bg_refresh_done: ${key} (${Object.keys(modelProviders).length} endpoints)`, - ) - memoryCache.set(key, modelProviders) - try { - await writeModelEndpoints(key, modelProviders) - } catch (error) { - console.error( - `[endpointCache] Error writing ${key} to file cache during background refresh:`, - error instanceof Error ? error.message : String(error), - ) - } - return modelProviders - } - return {} - })() - - inFlightEndpointFetches.set(key, bgPromise) - Promise.resolve(bgPromise) - .catch((err) => { - // Log background refresh failures for monitoring - console.error( - `[endpointCache] Background refresh failed for ${key}:`, - err instanceof Error ? err.message : String(err), - ) - }) - .finally(() => inFlightEndpointFetches.delete(key)) - } - return file } } catch { - // ignore file read errors; fall through + // ignore file read errors; fall through to network fetch } - // 3) Coalesce concurrent fetches - const inFlight = inFlightEndpointFetches.get(key) - if (inFlight) { - // Using console.log for cache layer logging (no provider access in utility functions) - console.log(`[endpointCache] coalesced_wait: ${key}`) - return inFlight - } - - // 4) Single network fetch for this key + // 3) Network fetch const signal = AbortSignal.timeout(30_000) - const fetchPromise = (async (): Promise => { - let modelProviders: ModelRecord = {} - modelProviders = await getOpenRouterModelEndpoints(modelId, undefined, signal) + let modelProviders: ModelRecord = {} - if (Object.keys(modelProviders).length > 0) { - console.log(`[endpointCache] network_fetch_done: ${key} (${Object.keys(modelProviders).length} endpoints)`) - // Update memory cache first - memoryCache.set(key, modelProviders) + modelProviders = await getOpenRouterModelEndpoints(modelId, undefined, signal) - // Best-effort persist - try { - await writeModelEndpoints(key, modelProviders) - } catch (error) { - console.error( - `[endpointCache] Error writing ${key} to file cache after network fetch:`, - error instanceof Error ? error.message : String(error), - ) - } + if (Object.keys(modelProviders).length > 0) { + memoryCache.set(key, modelProviders) - return modelProviders - } - - // Fallback to file cache if network returned empty (rare) try { - const file = await readModelEndpoints(key) - return file ?? {} - } catch { - return {} + await writeModelEndpoints(key, modelProviders) + } catch (error) { + console.error( + `[endpointCache] Error writing ${key} to file cache after network fetch:`, + error instanceof Error ? error.message : String(error), + ) } - })() - inFlightEndpointFetches.set(key, fetchPromise) + return modelProviders + } + + // Fallback to file cache if network returned empty (rare) try { - return await fetchPromise - } finally { - inFlightEndpointFetches.delete(key) + const file = await readModelEndpoints(key) + return file ?? {} + } catch { + return {} } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a3ef6ff49d..3d0f497a9e 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -832,6 +832,35 @@ export const webviewMessageHandler = async ( }, ] + // Include local providers (ollama, lmstudio, huggingface) when they are the active provider + if (activeProvider === "ollama") { + allFetches.push({ + key: "ollama", + options: { + provider: "ollama", + baseUrl: apiConfiguration.ollamaBaseUrl, + apiKey: apiConfiguration.ollamaApiKey, + }, + }) + } + if (activeProvider === "lmstudio") { + allFetches.push({ + key: "lmstudio", + options: { + provider: "lmstudio", + baseUrl: apiConfiguration.lmStudioBaseUrl, + }, + }) + } + if (activeProvider === "huggingface") { + allFetches.push({ + key: "huggingface", + options: { + provider: "huggingface", + }, + }) + } + // IO Intelligence (optional) if (apiConfiguration.ioIntelligenceApiKey) { allFetches.push({