From bbe4a611536de454202b96abcb3288e953485314 Mon Sep 17 00:00:00 2001 From: Automation Date: Sun, 24 May 2026 01:20:00 +0200 Subject: [PATCH] fix(reasoning): extend reasoning_content injection to Kimi K2 and other replay models The injectEmptyReasoningContentForToolCalls function only handled DeepSeek V4 models, leaving Kimi K2 and other reasoning models without reasoning_content when tool_calls were present. This caused Kimi models to enter an infinite loop, repeating the same tool call indefinitely because the reasoning_content was not passed back. Changes: - Use requiresReasoningReplay() instead of isDeepSeekReasoningModel() to detect all models that need reasoning_content injection - Add comprehensive tests for Kimi K2, Qoder, and all reasoning replay providers Fixes infinite loop when using Kimi K2 models through OmniRoute. --- open-sse/translator/helpers/schemaCoercion.ts | 21 +++-- tests/unit/schema-coercion.test.ts | 94 ++++++++++++++++++- 2 files changed, 105 insertions(+), 10 deletions(-) diff --git a/open-sse/translator/helpers/schemaCoercion.ts b/open-sse/translator/helpers/schemaCoercion.ts index aa503c1d7686..b87f4c7fde7e 100644 --- a/open-sse/translator/helpers/schemaCoercion.ts +++ b/open-sse/translator/helpers/schemaCoercion.ts @@ -1,4 +1,4 @@ -import { isDeepSeekReasoningModel } from "../../services/reasoningCache.ts"; +import { isDeepSeekReasoningModel, requiresReasoningReplay } from "../../services/reasoningCache.ts"; /** * Shared sanitizers for tool payloads that arrive from IDEs/SDKs with @@ -201,14 +201,17 @@ export function injectEmptyReasoningContentForToolCalls( provider: unknown, model: unknown ): unknown { - if ( - !Array.isArray(messages) || - !isDeepSeekReasoningModel({ - provider: String(provider ?? ""), - model: String(model ?? ""), - thinkingEnabled: true, - }) - ) { + const normalizedProvider = String(provider ?? ""); + const normalizedModel = String(model ?? ""); + + // Check if this provider/model requires reasoning replay (DeepSeek V4, Kimi K2, etc.) + const needsReasoning = requiresReasoningReplay({ + provider: normalizedProvider, + model: normalizedModel, + thinkingEnabled: true, + }); + + if (!Array.isArray(messages) || !needsReasoning) { return messages; } diff --git a/tests/unit/schema-coercion.test.ts b/tests/unit/schema-coercion.test.ts index 511310e5b682..c4424f3e878b 100644 --- a/tests/unit/schema-coercion.test.ts +++ b/tests/unit/schema-coercion.test.ts @@ -131,7 +131,17 @@ test("injectEmptyReasoningContentForToolCalls supports DeepSeek V4 models across } }); -test("injectEmptyReasoningContentForToolCalls skips non-DeepSeek V4 models", () => { +test("injectEmptyReasoningContentForToolCalls skips non-reasoning models", () => { + const messages = [{ role: "assistant", tool_calls: [{ id: "call_1" }] }]; + + const result = injectEmptyReasoningContentForToolCalls( + messages, + "deepseek", + "deepseek-v3" + ) as Array<{ reasoning_content?: string }>; + + assert.equal(result[0].reasoning_content, undefined); +}); const messages = [{ role: "assistant", tool_calls: [{ id: "call_1" }] }]; const result = injectEmptyReasoningContentForToolCalls( @@ -142,3 +152,85 @@ test("injectEmptyReasoningContentForToolCalls skips non-DeepSeek V4 models", () assert.equal(result[0].reasoning_content, undefined); }); + +test("injectEmptyReasoningContentForToolCalls supports Kimi K2 models", () => { + const messages = [ + { role: "user", content: "hello" }, + { role: "assistant", tool_calls: [{ id: "call_1" }] }, + ]; + + for (const model of ["kimi-k2", "kimi-k2.5", "kimi-k2.6", "kimi-k2-thinking"]) { + const result = injectEmptyReasoningContentForToolCalls( + messages, + "moonshot", + model + ) as Array<{ reasoning_content?: string }>; + + assert.equal(result[1].reasoning_content, "", `should inject reasoning_content for ${model}`); + } +}); + +test("injectEmptyReasoningContentForToolCalls supports Kimi K2 via Qoder provider", () => { + const messages = [ + { role: "user", content: "hello" }, + { role: "assistant", tool_calls: [{ id: "call_1" }] }, + ]; + + const result = injectEmptyReasoningContentForToolCalls( + messages, + "qoder", + "kimi-k2-thinking" + ) as Array<{ reasoning_content?: string }>; + + assert.equal(result[1].reasoning_content, ""); +}); + +test("injectEmptyReasoningContentForToolCalls supports all reasoning replay providers", () => { + const messages = [ + { role: "user", content: "hello" }, + { role: "assistant", tool_calls: [{ id: "call_1" }] }, + ]; + + const reasoningProviders = [ + { provider: "deepseek", model: "deepseek-chat" }, + { provider: "opencode-go", model: "some-model" }, + { provider: "siliconflow", model: "deepseek-r1" }, + { provider: "nebius", model: "qwq-32b" }, + { provider: "deepinfra", model: "deepseek-reasoner" }, + { provider: "sambanova", model: "qwen3-thinking" }, + { provider: "fireworks", model: "glm-5-thinking" }, + { provider: "together", model: "mimo-v2.5" }, + { provider: "xiaomi-mimo", model: "mimo-v2.5-pro" }, + ]; + + for (const { provider, model } of reasoningProviders) { + const result = injectEmptyReasoningContentForToolCalls( + messages, + provider, + model + ) as Array<{ reasoning_content?: string }>; + + assert.equal(result[1].reasoning_content, "", `should inject reasoning_content for ${provider}/${model}`); + } +}); + +test("injectEmptyReasoningContentForToolCalls skips non-reasoning models", () => { + const messages = [{ role: "assistant", tool_calls: [{ id: "call_1" }] }]; + + const nonReasoningModels = [ + { provider: "openai", model: "gpt-4" }, + { provider: "anthropic", model: "claude-sonnet-4" }, + { provider: "google", model: "gemini-pro" }, + { provider: "groq", model: "llama-3" }, + ]; + + for (const { provider, model } of nonReasoningModels) { + const result = injectEmptyReasoningContentForToolCalls( + messages, + provider, + model + ) as Array<{ reasoning_content?: string }>; + + assert.equal(result[0].reasoning_content, undefined, `should NOT inject reasoning_content for ${provider}/${model}`); + } +});