From 99b8b46ac0911b6af70d6bf424e79eaad37745ff Mon Sep 17 00:00:00 2001 From: jackjinke Date: Wed, 5 Aug 2026 16:29:33 +0800 Subject: [PATCH] fix(translator): make K3 reasoning preservation model-driven --- .../fixes/9496-kimi-k3-responses-replay.md | 2 +- .../translator/helpers/responsesApiHelper.ts | 4 +-- open-sse/translator/index.ts | 8 ++--- open-sse/utils/reasoningContentInjector.ts | 29 ++++++------------- .../unit/responses-translation-fixes.test.ts | 14 ++++++--- 5 files changed, 24 insertions(+), 33 deletions(-) diff --git a/changelog.d/fixes/9496-kimi-k3-responses-replay.md b/changelog.d/fixes/9496-kimi-k3-responses-replay.md index d6ba06fe78..4f7e3723a3 100644 --- a/changelog.d/fixes/9496-kimi-k3-responses-replay.md +++ b/changelog.d/fixes/9496-kimi-k3-responses-replay.md @@ -1 +1 @@ -- fix(translator): preserve Kimi K3 Responses reasoning through Kimi Coding Claude-format and native Moonshot requests, keeping it on the matching assistant tool call or completed turn instead of dropping it or carrying it across a user boundary (#9496) +- fix(translator): preserve authentic K3 Responses reasoning by model across providers, projecting it onto the matching assistant tool call or completed turn instead of dropping it or carrying it across a user boundary (#9496) diff --git a/open-sse/translator/helpers/responsesApiHelper.ts b/open-sse/translator/helpers/responsesApiHelper.ts index c0148956b8..2bda0a2229 100644 --- a/open-sse/translator/helpers/responsesApiHelper.ts +++ b/open-sse/translator/helpers/responsesApiHelper.ts @@ -2,7 +2,7 @@ * Convert OpenAI Responses API format to standard chat completions format. * Delegates to the canonical translator to avoid logic duplication. */ -import { shouldPreserveResponsesReasoningContent } from "../../utils/reasoningContentInjector.ts"; +import { requiresAuthenticReasoningContent } from "../../utils/reasoningContentInjector.ts"; import { openaiResponsesToOpenAIRequest } from "../request/openai-responses.ts"; import { toRecord } from "../request/openai-responses/helpers.ts"; @@ -23,7 +23,7 @@ export function convertResponsesApiFormat( credentials && typeof credentials === "object" && !Array.isArray(credentials) ? (credentials as Record) : {}; - const translationCredentials = shouldPreserveResponsesReasoningContent(provider, model) + const translationCredentials = requiresAuthenticReasoningContent(provider, model) ? { ...credentialRecord, _preserveReasoningContent: true } : credentials; const converted = openaiResponsesToOpenAIRequest( diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index da1c7963a6..aa9beae6be 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -13,10 +13,7 @@ import { providerHonorsOpenAIFormatCacheControl, resolveConnectionCacheOverride, } from "../utils/cacheControlPolicy.ts"; -import { - requiresAuthenticReasoningContent, - shouldPreserveResponsesReasoningContent, -} from "../utils/reasoningContentInjector.ts"; +import { requiresAuthenticReasoningContent } from "../utils/reasoningContentInjector.ts"; import { isInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts"; import { coerceToolSchemas, @@ -245,8 +242,7 @@ export function translateRequest( normalizedModel ); const preserveResponsesReasoning = - sourceFormat === FORMATS.OPENAI_RESPONSES && - shouldPreserveResponsesReasoningContent(normalizedProvider, normalizedModel); + sourceFormat === FORMATS.OPENAI_RESPONSES && requiresAuthenticReasoning; // Phase 2: Apply thinking budget control before normalization result = applyThinkingBudget(result); diff --git a/open-sse/utils/reasoningContentInjector.ts b/open-sse/utils/reasoningContentInjector.ts index b36a44f4fb..83d1cf4c1a 100644 --- a/open-sse/utils/reasoningContentInjector.ts +++ b/open-sse/utils/reasoningContentInjector.ts @@ -30,35 +30,24 @@ const THINKING_MODEL_PATTERNS: RegExp[] = [ /\bmimo\b/i, // xiaomi-tokenplan mimo family (e.g. xiaomi-tokenplan/mimo-v2.5-pro) ]; -const AUTHENTIC_REASONING_MODEL_PATTERN = /(?:^|\/)kimi-k(?:3|2\.7-code)(?:$|-)/i; +const K3_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)(?:kimi-)?k3(?:$|-)/i; +const NATIVE_K27_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)kimi-k2\.7-code(?:$|-)/i; /** - * Native Moonshot K3/K2.7 replay must use the original reasoning content. - * A fabricated placeholder changes preserved-thinking history and is not a - * valid substitute when the client and reasoning cache both lack the field. + * K3 requires authentic reasoning regardless of which provider serves it. + * Native Moonshot K2.7 retains the same preserved-thinking contract. Empty + * protocol markers remain valid only after client content and replay miss. */ export function requiresAuthenticReasoningContent(provider: unknown, model: unknown): boolean { + const normalizedModel = String(model ?? "").trim(); + if (K3_AUTHENTIC_REASONING_PATTERN.test(normalizedModel)) return true; + const normalizedProvider = String(provider ?? "") .trim() .toLowerCase(); - const normalizedModel = String(model ?? "").trim(); return ( (normalizedProvider === "moonshot" || normalizedProvider === "kimi") && - AUTHENTIC_REASONING_MODEL_PATTERN.test(normalizedModel) - ); -} - -export function shouldPreserveResponsesReasoningContent( - provider: unknown, - model: unknown -): boolean { - const normalizedProvider = String(provider ?? "") - .trim() - .toLowerCase(); - return ( - normalizedProvider === "kimi-coding" || - normalizedProvider === "kimi-coding-apikey" || - requiresAuthenticReasoningContent(normalizedProvider, model) + NATIVE_K27_AUTHENTIC_REASONING_PATTERN.test(normalizedModel) ); } diff --git a/tests/unit/responses-translation-fixes.test.ts b/tests/unit/responses-translation-fixes.test.ts index c4b981b1c7..d02f36736f 100644 --- a/tests/unit/responses-translation-fixes.test.ts +++ b/tests/unit/responses-translation-fixes.test.ts @@ -57,9 +57,13 @@ test("production Responses conversion preserves Kimi K3 reasoning history", () = }; for (const { provider, model } of [ + { provider: "kimi-coding", model: "k3" }, { provider: "kimi-coding-apikey", model: "k3-256k" }, { provider: "moonshot", model: "kimi-k3" }, { provider: "kimi", model: "kimi-k3" }, + { provider: "some-other", model: "k3" }, + { provider: "some-other", model: "k3-256k" }, + { provider: "some-other", model: "kimi-k3" }, ]) { const converted = convertResponsesApiFormat(body, {}, provider, model) as { messages: Array>; @@ -67,10 +71,12 @@ test("production Responses conversion preserves Kimi K3 reasoning history", () = assert.equal(converted.messages[1].reasoning_content, "I should search first."); } - const generic = convertResponsesApiFormat(body, {}, "openai", "gpt-5") as { - messages: Array>; - }; - assert.equal(Object.hasOwn(generic.messages[1], "reasoning_content"), false); + for (const provider of ["kimi-coding", "kimi-coding-apikey"]) { + const generic = convertResponsesApiFormat(body, {}, provider, "kimi-k2.6") as { + messages: Array>; + }; + assert.equal(Object.hasOwn(generic.messages[1], "reasoning_content"), false, provider); + } }); test("Responses→Chat: input_image converted to image_url with detail", () => {