diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 860ca5155c..77855fd435 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -21,7 +21,7 @@ import { getVercelAiGatewayModels } from "./vercel-ai-gateway" import { getRequestyModels } from "./requesty" import { getLiteLLMModels } from "./litellm" import { GetModelsOptions } from "../../../shared/api" -import { getOllamaModels } from "./ollama" +import { discoverOllamaModelsWithSorting } from "./ollama" import { getLMStudioModels } from "./lmstudio" import { getRooModels } from "./roo" @@ -72,14 +72,21 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise response, async (error: AxiosError) => { - const axiosConfig = error.config as any + const axiosConfig = error.config as OllamaInternalAxiosConfig | undefined + if (!axiosConfig) { + return Promise.reject(error) + } axiosConfig.__retryCount = axiosConfig.__retryCount || 0 if (axiosConfig.__retryCount >= config.retries) { @@ -94,7 +106,7 @@ function setupRetryInterceptor(instance: AxiosInstance, config: { retries: numbe function setupLoggingInterceptor(instance: AxiosInstance) { instance.interceptors.request.use((config: InternalAxiosRequestConfig) => { - ;(config as any).metadata = { startTime: Date.now() } + ;(config as OllamaInternalAxiosConfig).metadata = { startTime: Date.now() } console.debug("[Ollama] Request:", { method: config.method?.toUpperCase(), url: `${config.baseURL}${config.url}`, @@ -106,7 +118,7 @@ function setupLoggingInterceptor(instance: AxiosInstance) { instance.interceptors.response.use( (response: AxiosResponse) => { - const startTime = (response.config as any).metadata?.startTime + const startTime = (response.config as OllamaInternalAxiosConfig).metadata?.startTime const duration = startTime ? Date.now() - startTime : undefined console.debug("[Ollama] Response:", { status: response.status, @@ -118,7 +130,7 @@ function setupLoggingInterceptor(instance: AxiosInstance) { return response }, (error: AxiosError) => { - const startTime = (error.config as any)?.metadata?.startTime + const startTime = (error.config as OllamaInternalAxiosConfig | undefined)?.metadata?.startTime const duration = startTime ? Date.now() - startTime : undefined console.error("[Ollama] Error:", { code: error.code, @@ -361,7 +373,7 @@ export async function discoverOllamaModelsWithSorting( const parsedDetail = OllamaModelInfoResponseSchema.safeParse(detailResponse.data) if (!parsedDetail.success) { // If validation fails, check if required fields exist in raw data - const rawData = detailResponse.data as any + const rawData = detailResponse.data as Partial if (!rawData?.details?.family || !rawData?.details?.parameter_size) { if (config?.enableLogging) { console.warn(`Invalid response for model ${ollamaModel.name}: missing required fields`) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 206c30496b..3f33dc1d3d 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -252,7 +252,10 @@ describe("webviewMessageHandler - requestOllamaModels", () => { type: "requestOllamaModels", }) - expect(mockFlushModels).toHaveBeenCalledWith({ provider: "ollama", baseUrl: "http://localhost:1234" }, true) + expect(mockFlushModels).toHaveBeenCalledWith( + { provider: "ollama", baseUrl: "http://localhost:1234", apiKey: undefined }, + false, + ) expect(mockDiscoverOllamaModels).toHaveBeenCalledWith( "http://localhost:1234", undefined, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 345b7452eb..42f78e4ac1 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -985,12 +985,16 @@ export const webviewMessageHandler = async ( case "requestOllamaModels": { const { apiConfiguration: ollamaApiConfig } = await provider.getState() try { - const ollamaOptions = { - provider: "ollama" as const, - baseUrl: ollamaApiConfig.ollamaBaseUrl, - apiKey: ollamaApiConfig.ollamaApiKey, - } - await flushModels(ollamaOptions, true) + // Invalidate stale cache without refetching (discoverOllamaModelsWithSorting + // will perform the single authoritative API call below) + await flushModels( + { + provider: "ollama" as const, + baseUrl: ollamaApiConfig.ollamaBaseUrl, + apiKey: ollamaApiConfig.ollamaApiKey, + }, + false, + ) const result = await discoverOllamaModelsWithSorting( ollamaApiConfig.ollamaBaseUrl, @@ -1004,20 +1008,19 @@ export const webviewMessageHandler = async ( ) // Convert modelsWithTools array to Record for compatibility - const modelsWithToolsRecord: Record = {} + const modelsWithToolsRecord: ModelRecord = {} for (const model of result.modelsWithTools) { modelsWithToolsRecord[model.name] = model.modelInfo } - // Always send the models message if we have any results - if (result.totalCount > 0) { - provider.postMessageToWebview({ - type: "ollamaModels", - ollamaModels: modelsWithToolsRecord, - ollamaModelsWithTools: result.modelsWithTools, - modelsWithoutTools: result.modelsWithoutTools, - }) - } + // Always send models to the webview, even when empty, so the UI + // can reflect that no models were found instead of silently stalling. + provider.postMessageToWebview({ + type: "ollamaModels", + ollamaModels: modelsWithToolsRecord, + ollamaModelsWithTools: result.modelsWithTools, + modelsWithoutTools: result.modelsWithoutTools, + }) } catch (error) { console.debug("Ollama models fetch failed:", error) } @@ -1065,17 +1068,16 @@ export const webviewMessageHandler = async ( const apiKey = message.ollamaApiKey ?? ollamaApiConfig.ollamaApiKey try { - const ollamaOptions = { - provider: "ollama" as const, - baseUrl: baseUrl, - apiKey: apiKey, - ollamaModelDiscoveryTimeout: ollamaApiConfig.ollamaModelDiscoveryTimeout, - ollamaMaxRetries: ollamaApiConfig.ollamaMaxRetries, - ollamaRetryDelay: ollamaApiConfig.ollamaRetryDelay, - ollamaEnableLogging: ollamaApiConfig.ollamaEnableLogging, - } - - await flushModels(ollamaOptions, true) + // Invalidate stale cache without refetching (discoverOllamaModelsWithSorting + // will perform the single authoritative API call below) + await flushModels( + { + provider: "ollama" as const, + baseUrl: baseUrl, + apiKey: apiKey, + }, + false, + ) const result = await discoverOllamaModelsWithSorting(baseUrl, apiKey, { modelDiscoveryTimeout: ollamaApiConfig.ollamaModelDiscoveryTimeout, @@ -1087,7 +1089,7 @@ export const webviewMessageHandler = async ( const durationMs = Date.now() - startTime // Convert modelsWithTools array to Record for compatibility - const modelsWithToolsRecord: Record = {} + const modelsWithToolsRecord: ModelRecord = {} for (const model of result.modelsWithTools) { modelsWithToolsRecord[model.name] = model.modelInfo } @@ -1104,14 +1106,15 @@ export const webviewMessageHandler = async ( }) } - // Always send the models message if we have any results + // Always send models to the webview so the UI stays in sync + provider.postMessageToWebview({ + type: "ollamaModels", + ollamaModels: modelsWithToolsRecord, + ollamaModelsWithTools: result.modelsWithTools, + modelsWithoutTools: result.modelsWithoutTools, + }) + if (result.totalCount > 0) { - provider.postMessageToWebview({ - type: "ollamaModels", - ollamaModels: modelsWithToolsRecord, - ollamaModelsWithTools: result.modelsWithTools, - modelsWithoutTools: result.modelsWithoutTools, - }) provider.postMessageToWebview({ type: "ollamaModelsRefreshResult", success: true,