From 2177a8c387ec2acdb6bebf9c3bfd385ab80d052c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 25 Nov 2025 03:23:45 +0000 Subject: [PATCH] fix: improve tool call handling for OpenAI-compatible servers like ik_llama.cpp - Modified base-openai-compatible-provider.ts to better handle tool calls - Tool calls are now yielded when any finish_reason is present, not just "tool_calls" - Added fallback to yield tool calls at stream end if not already yielded - Added tracking to prevent duplicate tool call yields - Ensures tool calls with valid ID and name are always yielded - Fixes issue #9551 where Kimi K2 Thinking native tool calls were not working --- .../base-openai-compatible-provider.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index 5aee7267b3b..772cb76e948 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -130,6 +130,7 @@ export abstract class BaseOpenAiCompatibleProvider let lastUsage: OpenAI.CompletionUsage | undefined const activeToolCallIds = new Set() + let hasEmittedToolCallEnd = false for await (const chunk of stream) { // Check for provider-specific error responses (e.g., MiniMax base_resp) @@ -177,12 +178,14 @@ export abstract class BaseOpenAiCompatibleProvider } } - // Emit tool_call_end events when finish_reason is "tool_calls" - // This ensures tool calls are finalized even if the stream doesn't properly close - if (finishReason === "tool_calls" && activeToolCallIds.size > 0) { + // Emit tool_call_end events when ANY finish_reason is present (not just "tool_calls") + // This ensures compatibility with various OpenAI-compatible servers including ik_llama.cpp + // that may not set finish_reason to "tool_calls" specifically + if (finishReason && activeToolCallIds.size > 0 && !hasEmittedToolCallEnd) { for (const id of activeToolCallIds) { yield { type: "tool_call_end", id } } + hasEmittedToolCallEnd = true activeToolCallIds.clear() } @@ -191,6 +194,15 @@ export abstract class BaseOpenAiCompatibleProvider } } + // Fallback: If stream ends with active tool calls that weren't finalized + // This is crucial for servers like ik_llama.cpp that may not send proper finish_reason + if (activeToolCallIds.size > 0 && !hasEmittedToolCallEnd) { + for (const id of activeToolCallIds) { + yield { type: "tool_call_end", id } + } + activeToolCallIds.clear() + } + if (lastUsage) { yield this.processUsageMetrics(lastUsage, this.getModel().info) }