From 9869b06b9ebefd6ce947928fe6430f87dffa310e Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 28 Jan 2026 19:47:36 +0000 Subject: [PATCH] fix(ollama): add logging and diagnostics for debugging Ollama connection issues - Add logging when models are filtered out due to missing tool support - Add warning when selected model is not in the tool-capable models list - Update parseOllamaModel to return filteredReason for better diagnostics - Log request info (model ID, base URL) when starting Ollama requests This helps diagnose issues like #11049 where Ollama models appear unresponsive. The logging output will show: - Which models are being filtered and why - When a model may not support native tool calling - Request details for debugging connectivity issues --- .../fetchers/__tests__/ollama.test.ts | 45 ++++++++++--------- src/api/providers/fetchers/ollama.ts | 28 ++++++++++-- src/api/providers/native-ollama.ts | 15 ++++++- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/api/providers/fetchers/__tests__/ollama.test.ts b/src/api/providers/fetchers/__tests__/ollama.test.ts index 59663bc4959..829d9535fb3 100644 --- a/src/api/providers/fetchers/__tests__/ollama.test.ts +++ b/src/api/providers/fetchers/__tests__/ollama.test.ts @@ -15,9 +15,9 @@ describe("Ollama Fetcher", () => { describe("parseOllamaModel", () => { it("should correctly parse Ollama model info", () => { const modelData = ollamaModelsData["qwen3-2to16:latest"] - const parsedModel = parseOllamaModel(modelData) + const { modelInfo } = parseOllamaModel(modelData) - expect(parsedModel).toEqual({ + expect(modelInfo).toEqual({ maxTokens: 40960, contextWindow: 40960, supportsImages: false, @@ -39,9 +39,9 @@ describe("Ollama Fetcher", () => { }, } - const parsedModel = parseOllamaModel(modelDataWithNullFamilies as any) + const { modelInfo } = parseOllamaModel(modelDataWithNullFamilies as any) - expect(parsedModel).toEqual({ + expect(modelInfo).toEqual({ maxTokens: 40960, contextWindow: 40960, supportsImages: false, @@ -54,16 +54,18 @@ describe("Ollama Fetcher", () => { }) }) - it("should return null when capabilities does not include 'tools'", () => { + it("should return null with reason when capabilities does not include 'tools'", () => { const modelDataWithoutTools = { ...ollamaModelsData["qwen3-2to16:latest"], capabilities: ["completion"], // No "tools" capability } - const parsedModel = parseOllamaModel(modelDataWithoutTools as any) + const { modelInfo, filteredReason } = parseOllamaModel(modelDataWithoutTools as any, "test-model") // Models without tools capability are filtered out (return null) - expect(parsedModel).toBeNull() + expect(modelInfo).toBeNull() + expect(filteredReason).toContain("test-model") + expect(filteredReason).toContain("do not include 'tools'") }) it("should return model info when capabilities includes 'tools'", () => { @@ -72,22 +74,25 @@ describe("Ollama Fetcher", () => { capabilities: ["completion", "tools"], // Has "tools" capability } - const parsedModel = parseOllamaModel(modelDataWithTools as any) + const { modelInfo, filteredReason } = parseOllamaModel(modelDataWithTools as any) - expect(parsedModel).not.toBeNull() - expect(parsedModel!.contextWindow).toBeGreaterThan(0) + expect(modelInfo).not.toBeNull() + expect(modelInfo!.contextWindow).toBeGreaterThan(0) + expect(filteredReason).toBeUndefined() }) - it("should return null when capabilities is undefined (no tool support)", () => { + it("should return null with reason when capabilities is undefined (no tool support)", () => { const modelDataWithoutCapabilities = { ...ollamaModelsData["qwen3-2to16:latest"], capabilities: undefined, // No capabilities array } - const parsedModel = parseOllamaModel(modelDataWithoutCapabilities as any) + const { modelInfo, filteredReason } = parseOllamaModel(modelDataWithoutCapabilities as any, "test-model") // Models without explicit tools capability are filtered out - expect(parsedModel).toBeNull() + expect(modelInfo).toBeNull() + expect(filteredReason).toContain("test-model") + expect(filteredReason).toContain("no capabilities reported") }) it("should return null when model has vision but no tools capability", () => { @@ -96,10 +101,10 @@ describe("Ollama Fetcher", () => { capabilities: ["completion", "vision"], } - const parsedModel = parseOllamaModel(modelDataWithVision as any) + const { modelInfo } = parseOllamaModel(modelDataWithVision as any) // No "tools" capability means filtered out - expect(parsedModel).toBeNull() + expect(modelInfo).toBeNull() }) it("should return model with both vision and tools when both capabilities present", () => { @@ -108,11 +113,11 @@ describe("Ollama Fetcher", () => { capabilities: ["completion", "vision", "tools"], } - const parsedModel = parseOllamaModel(modelDataWithBoth as any) + const { modelInfo } = parseOllamaModel(modelDataWithBoth as any) - expect(parsedModel).not.toBeNull() - expect(parsedModel!.supportsImages).toBe(true) - expect(parsedModel!.contextWindow).toBeGreaterThan(0) + expect(modelInfo).not.toBeNull() + expect(modelInfo!.supportsImages).toBe(true) + expect(modelInfo!.contextWindow).toBeGreaterThan(0) }) }) @@ -177,7 +182,7 @@ describe("Ollama Fetcher", () => { expect(Object.keys(result).length).toBe(1) expect(result[modelName]).toBeDefined() - const expectedParsedDetails = parseOllamaModel(mockApiShowResponse as any) + const { modelInfo: expectedParsedDetails } = parseOllamaModel(mockApiShowResponse as any) expect(result[modelName]).toEqual(expectedParsedDetails) }) diff --git a/src/api/providers/fetchers/ollama.ts b/src/api/providers/fetchers/ollama.ts index ba5b1c1d5d9..1e31365f5e7 100644 --- a/src/api/providers/fetchers/ollama.ts +++ b/src/api/providers/fetchers/ollama.ts @@ -37,7 +37,10 @@ type OllamaModelsResponse = z.infer type OllamaModelInfoResponse = z.infer -export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo | null => { +export const parseOllamaModel = ( + rawModel: OllamaModelInfoResponse, + modelName?: string, +): { modelInfo: ModelInfo | null; filteredReason?: string } => { const contextKey = Object.keys(rawModel.model_info).find((k) => k.includes("context_length")) const contextWindow = contextKey && typeof rawModel.model_info[contextKey] === "number" ? rawModel.model_info[contextKey] : undefined @@ -45,7 +48,10 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo | // Filter out models that don't support tools. Models without tool capability won't work. const supportsTools = rawModel.capabilities?.includes("tools") ?? false if (!supportsTools) { - return null + const reason = rawModel.capabilities + ? `Model '${modelName || "unknown"}' capabilities (${rawModel.capabilities.join(", ")}) do not include 'tools'` + : `Model '${modelName || "unknown"}' has no capabilities reported (Ollama may need to be updated)` + return { modelInfo: null, filteredReason: reason } } const modelInfo: ModelInfo = Object.assign({}, ollamaDefaultModelInfo, { @@ -56,7 +62,7 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo | maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow, }) - return modelInfo + return { modelInfo } } export async function getOllamaModels( @@ -84,6 +90,8 @@ export async function getOllamaModels( let modelInfoPromises = [] if (parsedResponse.success) { + const filteredModels: string[] = [] + for (const ollamaModel of parsedResponse.data.models) { modelInfoPromises.push( axios @@ -95,16 +103,28 @@ export async function getOllamaModels( { headers }, ) .then((ollamaModelInfo) => { - const modelInfo = parseOllamaModel(ollamaModelInfo.data) + const { modelInfo, filteredReason } = parseOllamaModel( + ollamaModelInfo.data, + ollamaModel.name, + ) // Only include models that support native tools if (modelInfo) { models[ollamaModel.name] = modelInfo + } else if (filteredReason) { + filteredModels.push(filteredReason) } }), ) } await Promise.all(modelInfoPromises) + + // Log filtered models to help users understand why models aren't appearing + if (filteredModels.length > 0) { + console.warn( + `[Ollama] ${filteredModels.length} model(s) filtered out due to missing tool support:\n${filteredModels.join("\n")}`, + ) + } } else { console.error(`Error parsing Ollama models response: ${JSON.stringify(parsedResponse.error, null, 2)}`) } diff --git a/src/api/providers/native-ollama.ts b/src/api/providers/native-ollama.ts index 99c1dc03cfa..e10895d4c69 100644 --- a/src/api/providers/native-ollama.ts +++ b/src/api/providers/native-ollama.ts @@ -206,9 +206,22 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { const client = this.ensureClient() - const { id: modelId } = await this.fetchModel() + const { id: modelId, info: modelInfo } = await this.fetchModel() const useR1Format = modelId.toLowerCase().includes("deepseek-r1") + // Log request info for debugging + const baseUrl = this.options.ollamaBaseUrl || "http://localhost:11434" + console.log(`[Ollama] Starting request to model '${modelId}' at ${baseUrl}`) + + // Warn if the model is not in the fetched models list (may indicate missing tool support) + if (!this.models[modelId]) { + console.warn( + `[Ollama] Warning: Model '${modelId}' was not found in the list of tool-capable models. ` + + `This may indicate the model does not support native tool calling. ` + + `Check if your Ollama version reports capabilities by running: ollama show ${modelId}`, + ) + } + const ollamaMessages: Message[] = [ { role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages),