From b27ea81f8535d99315d81b58d0eebbdf9109ef02 Mon Sep 17 00:00:00 2001 From: jackjinke Date: Sat, 8 Aug 2026 23:14:57 +0800 Subject: [PATCH] refactor(reasoning): use replay policy for K3 --- open-sse/services/reasoningCache.ts | 10 +++++ .../translator/helpers/responsesApiHelper.ts | 8 +++- open-sse/translator/index.ts | 44 ++++++++++--------- open-sse/utils/reasoningContentInjector.ts | 29 +++--------- tests/unit/service-reasoning-cache.test.ts | 23 ++++++++-- 5 files changed, 66 insertions(+), 48 deletions(-) diff --git a/open-sse/services/reasoningCache.ts b/open-sse/services/reasoningCache.ts index e23a388d6a..dc81b14e69 100644 --- a/open-sse/services/reasoningCache.ts +++ b/open-sse/services/reasoningCache.ts @@ -64,6 +64,8 @@ const REASONING_REPLAY_MODEL_PATTERNS = [ ]; const DEEPSEEK_V4_MODEL_PATTERN = /deepseek[-/]v4[-.](flash|pro)/i; +const K3_REASONING_REPLAY_MODEL_PATTERN = /(?:^|\/)(?:kimi-)?k3(?:$|-)/i; +const NATIVE_K27_REASONING_REPLAY_MODEL_PATTERN = /(?:^|\/)kimi-k2\.7-code(?:$|-)/i; export function isDeepSeekReasoningModel(params: { provider: string; @@ -94,6 +96,14 @@ export function requiresReasoningReplay(params: { if (normalizedInterleavedField === "reasoning_content") return true; if (normalizedInterleavedField === "reasoning_details") return false; + if (K3_REASONING_REPLAY_MODEL_PATTERN.test(normalizedModel)) return true; + if ( + (normalizedProvider === "moonshot" || normalizedProvider === "kimi") && + NATIVE_K27_REASONING_REPLAY_MODEL_PATTERN.test(normalizedModel) + ) { + return true; + } + // DeepSeek legacy reasoner family has an inverse contract: do not replay. if (/deepseek-reasoner/i.test(normalizedModel) || /deepseek-r1/i.test(normalizedModel)) { return false; diff --git a/open-sse/translator/helpers/responsesApiHelper.ts b/open-sse/translator/helpers/responsesApiHelper.ts index 2bda0a2229..625daf174f 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 { requiresAuthenticReasoningContent } from "../../utils/reasoningContentInjector.ts"; +import { requiresReasoningReplay } from "../../services/reasoningCache.ts"; import { openaiResponsesToOpenAIRequest } from "../request/openai-responses.ts"; import { toRecord } from "../request/openai-responses/helpers.ts"; @@ -23,7 +23,11 @@ export function convertResponsesApiFormat( credentials && typeof credentials === "object" && !Array.isArray(credentials) ? (credentials as Record) : {}; - const translationCredentials = requiresAuthenticReasoningContent(provider, model) + const translationCredentials = requiresReasoningReplay({ + provider: String(provider ?? ""), + model: String(model ?? ""), + allowLegacyFallback: false, + }) ? { ...credentialRecord, _preserveReasoningContent: true } : credentials; const converted = openaiResponsesToOpenAIRequest( diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index 583f57e030..b23d2ac6ee 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -13,7 +13,6 @@ import { providerHonorsOpenAIFormatCacheControl, resolveConnectionCacheOverride, } from "../utils/cacheControlPolicy.ts"; -import { requiresAuthenticReasoningContent } from "../utils/reasoningContentInjector.ts"; import { isInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts"; import { coerceToolSchemas, @@ -162,7 +161,11 @@ function isReasoningOnlyReplayTarget(provider: unknown, model: unknown): boolean /(^|\/)deepseek/i.test(normalizedModel) || normalizedProvider === "xiaomi-mimo" || /(^|\/)mimo/i.test(normalizedModel) || - requiresAuthenticReasoningContent(normalizedProvider, normalizedModel) + requiresReasoningReplay({ + provider: normalizedProvider, + model: normalizedModel, + allowLegacyFallback: false, + }) ); } @@ -237,12 +240,13 @@ export function translateRequest( const normalizedModel = String(model ?? ""); const isKimiCoding = normalizedProvider === "kimi-coding" || normalizedProvider === "kimi-coding-apikey"; - const requiresAuthenticReasoning = requiresAuthenticReasoningContent( - normalizedProvider, - normalizedModel - ); + const requiresExplicitReasoningReplay = requiresReasoningReplay({ + provider: normalizedProvider, + model: normalizedModel, + allowLegacyFallback: false, + }); const preserveResponsesReasoning = - sourceFormat === FORMATS.OPENAI_RESPONSES && requiresAuthenticReasoning; + sourceFormat === FORMATS.OPENAI_RESPONSES && requiresExplicitReasoningReplay; // Phase 2: Apply thinking budget control before normalization result = applyThinkingBudget(result); @@ -379,18 +383,16 @@ export function translateRequest( provider: normalizedProvider, model: normalizedModel, }); - const isReasoner = - requiresAuthenticReasoning || - requiresReasoningReplay({ + const isReasoner = requiresReasoningReplay({ + provider: normalizedProvider, + model: normalizedModel, + thinkingEnabled: hasThinkingConfig(result), + supportsReasoning: supportsReasoning({ provider: normalizedProvider, model: normalizedModel, - thinkingEnabled: hasThinkingConfig(result), - supportsReasoning: supportsReasoning({ - provider: normalizedProvider, - model: normalizedModel, - }), - interleavedField: resolvedCapabilities?.interleavedField ?? null, - }); + }), + interleavedField: resolvedCapabilities?.interleavedField ?? null, + }); // Always normalize to clean OpenAI format when target is OpenAI // This handles hybrid requests (e.g., OpenAI messages + Claude tools) @@ -461,7 +463,7 @@ export function translateRequest( if ( targetFormat === FORMATS.OPENAI && - !requiresAuthenticReasoning && + !requiresExplicitReasoningReplay && result.messages && Array.isArray(result.messages) ) { @@ -579,7 +581,7 @@ export function translateRequest( } continue; } - if (requiresAuthenticReasoning) continue; + if (requiresExplicitReasoningReplay) continue; msg.content.splice(firstToolUseIdx, 0, { type: "thinking", thinking: NON_ANTHROPIC_THINKING_PLACEHOLDER, @@ -616,7 +618,7 @@ export function translateRequest( // Native Moonshot K3/K2.7 accepts only the real prior reasoning. If it // was not supplied and the cache missed, leave it absent so upstream can // enforce its contract instead of corrupting history with a placeholder. - if (requiresAuthenticReasoning) { + if (requiresExplicitReasoningReplay) { if (msg.reasoning_content === "") delete msg.reasoning_content; continue; } @@ -629,7 +631,7 @@ export function translateRequest( // deepseek-v4-flash accepts an ABSENT reasoning_content field (the 400 is // specific to empty-string, and even that is endpoint-dependent). Omit // the field instead; providers that genuinely enforce the contract - // (kimi-coding, moonshot authentic-reasoning) have their own paths above. + // (kimi-coding, moonshot reasoning replay) have their own paths above. if ((hasToolCalls || shouldReplayReasoningOnly) && !msg.reasoning_content) { if (requiresReasoningContentPresence(normalizedProvider, normalizedModel)) { msg.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER; diff --git a/open-sse/utils/reasoningContentInjector.ts b/open-sse/utils/reasoningContentInjector.ts index 83d1cf4c1a..375057cf77 100644 --- a/open-sse/utils/reasoningContentInjector.ts +++ b/open-sse/utils/reasoningContentInjector.ts @@ -13,6 +13,8 @@ * that proxy to thinking-mode models. */ +import { requiresReasoningReplay } from "../services/reasoningCache.ts"; + const PLACEHOLDER = " "; type JsonRecord = Record; @@ -30,27 +32,6 @@ const THINKING_MODEL_PATTERNS: RegExp[] = [ /\bmimo\b/i, // xiaomi-tokenplan mimo family (e.g. xiaomi-tokenplan/mimo-v2.5-pro) ]; -const K3_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)(?:kimi-)?k3(?:$|-)/i; -const NATIVE_K27_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)kimi-k2\.7-code(?:$|-)/i; - -/** - * 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(); - return ( - (normalizedProvider === "moonshot" || normalizedProvider === "kimi") && - NATIVE_K27_AUTHENTIC_REASONING_PATTERN.test(normalizedModel) - ); -} - export function isThinkingMessageModel(model: string | undefined | null): boolean { if (!model || typeof model !== "string") return false; return THINKING_MODEL_PATTERNS.some((re) => re.test(model)); @@ -65,7 +46,11 @@ export function shouldInjectReasoningContentPlaceholder( .toLowerCase(); return ( (normalizedProvider === "moonshot" || normalizedProvider === "kimi") && - !requiresAuthenticReasoningContent(normalizedProvider, model) && + !requiresReasoningReplay({ + provider: normalizedProvider, + model: String(model ?? ""), + allowLegacyFallback: false, + }) && isThinkingMessageModel(model) ); } diff --git a/tests/unit/service-reasoning-cache.test.ts b/tests/unit/service-reasoning-cache.test.ts index 7b3a2490df..7b7df6a222 100644 --- a/tests/unit/service-reasoning-cache.test.ts +++ b/tests/unit/service-reasoning-cache.test.ts @@ -66,8 +66,8 @@ describe("reasoningCache helpers", () => { it("returns false for reasoning_details interleaved field", () => { assert.equal( mod.requiresReasoningReplay({ - provider: "any", - model: "any", + provider: "moonshot", + model: "k3", interleavedField: "reasoning_details", }), false @@ -116,6 +116,9 @@ describe("reasoningCache helpers", () => { it("detects native Kimi thinking model IDs without matching unrelated aliases", () => { for (const model of [ + "k3", + "k3-256k", + "kimi-k3", "kimi-k2", "kimi-k2.6", "kimi-k2.6-thinking", @@ -126,11 +129,25 @@ describe("reasoningCache helpers", () => { assert.equal(mod.requiresReasoningReplay({ provider: "some-other", model }), true, model); } - for (const model of ["k3", "moonshot-v1-8k", "kimi-latest"]) { + for (const model of ["moonshot-v1-8k", "kimi-latest"]) { assert.equal(mod.requiresReasoningReplay({ provider: "some-other", model }), false, model); } }); + it("keeps K3 and native Moonshot K2.7 replay explicit", () => { + for (const model of ["k3", "k3-256k", "kimi-k3", "kimi-k2.7-code"]) { + assert.equal( + mod.requiresReasoningReplay({ + provider: "moonshot", + model, + allowLegacyFallback: false, + }), + true, + model + ); + } + }); + it("returns false when allowLegacyFallback is false and no explicit signal", () => { assert.equal( mod.requiresReasoningReplay({