fix(translator): make K3 reasoning preservation model-driven

This commit is contained in:
jackjinke
2026-08-05 16:29:33 +08:00
committed by diegosouzapw
parent f2ca3f05bc
commit 99b8b46ac0
5 changed files with 24 additions and 33 deletions

View File

@@ -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)

View File

@@ -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<string, unknown>)
: {};
const translationCredentials = shouldPreserveResponsesReasoningContent(provider, model)
const translationCredentials = requiresAuthenticReasoningContent(provider, model)
? { ...credentialRecord, _preserveReasoningContent: true }
: credentials;
const converted = openaiResponsesToOpenAIRequest(

View File

@@ -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);

View File

@@ -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)
);
}

View File

@@ -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<Record<string, unknown>>;
@@ -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<Record<string, unknown>>;
};
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<Record<string, unknown>>;
};
assert.equal(Object.hasOwn(generic.messages[1], "reasoning_content"), false, provider);
}
});
test("Responses→Chat: input_image converted to image_url with detail", () => {