refactor(reasoning): use replay policy for K3

This commit is contained in:
jackjinke
2026-08-08 23:14:57 +08:00
committed by diegosouzapw
parent 9aab5f2e51
commit 7d044640ca
5 changed files with 66 additions and 48 deletions

View File

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

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

View File

@@ -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);
@@ -399,18 +403,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)
@@ -481,7 +483,7 @@ export function translateRequest(
if (
targetFormat === FORMATS.OPENAI &&
!requiresAuthenticReasoning &&
!requiresExplicitReasoningReplay &&
result.messages &&
Array.isArray(result.messages)
) {
@@ -599,7 +601,7 @@ export function translateRequest(
}
continue;
}
if (requiresAuthenticReasoning) continue;
if (requiresExplicitReasoningReplay) continue;
msg.content.splice(firstToolUseIdx, 0, {
type: "thinking",
thinking: NON_ANTHROPIC_THINKING_PLACEHOLDER,
@@ -636,7 +638,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;
}
@@ -649,7 +651,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;

View File

@@ -13,6 +13,8 @@
* that proxy to thinking-mode models.
*/
import { requiresReasoningReplay } from "../services/reasoningCache.ts";
const PLACEHOLDER = " ";
type JsonRecord = Record<string, unknown>;
@@ -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)
);
}

View File

@@ -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({