fix(translator): replay cached Kimi reasoning before fallback

This commit is contained in:
jackjinke
2026-08-05 17:36:52 +08:00
committed by diegosouzapw
parent 2effd7140a
commit 8d05fecd09
3 changed files with 50 additions and 22 deletions

View File

@@ -1 +1 @@
- 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)
- fix(translator): preserve authentic K3 Responses reasoning by model across providers, keep it on the matching assistant turn, and make Kimi Coding prefer client reasoning then cached replay before its empty-marker fallback (#9496)

View File

@@ -501,7 +501,7 @@ export function translateRequest(
// isReasoner / normalizedProvider / normalizedModel / resolvedCapabilities were
// resolved up-front (before the OpenAI-format filter) so the #4849 reasoning strip
// could honor reasoning-replay providers.
if (isReasoner && !isKimiCoding && result.messages && Array.isArray(result.messages)) {
if (isReasoner && result.messages && Array.isArray(result.messages)) {
const canReplayReasoningOnly = isReasoningOnlyReplayTarget(normalizedProvider, normalizedModel);
for (const [messageIndex, msg] of result.messages.entries()) {
@@ -550,29 +550,51 @@ export function translateRequest(
// Has tool_use blocks but no thinking block yet.
// Reasoning models (Kimi K2, etc.) require a thinking block before tool_use
// on multi-turn or they regenerate the same tool call infinitely.
const hasThinkingBlock = msg.content.some(
const thinkingBlock = msg.content.find(
(b) => b?.type === "thinking" || b?.type === "redacted_thinking"
);
if (hasThinkingBlock) continue;
const hasNonEmptyClientThinking =
thinkingBlock?.type === "thinking" &&
typeof thinkingBlock.thinking === "string" &&
thinkingBlock.thinking.trim().length > 0;
if (thinkingBlock && (!isKimiCoding || hasNonEmptyClientThinking)) continue;
const toolUseBlocks = msg.content.filter((b) => b?.type === "tool_use");
const firstToolUseId = toolUseBlocks[0]?.id;
const firstToolUseIdx = msg.content.findIndex((b) => b?.type === "tool_use");
// Try reasoning cache first
// Client reasoning wins above. Otherwise try authentic replay before
// retaining Kimi Code's empty protocol marker as the final fallback.
if (firstToolUseId) {
const cached = lookupReasoning(firstToolUseId);
if (cached) {
msg.content.splice(firstToolUseIdx, 0, {
type: "thinking",
thinking: cached,
});
if (thinkingBlock) {
thinkingBlock.type = "thinking";
thinkingBlock.thinking = cached;
delete thinkingBlock.data;
delete thinkingBlock.signature;
} else {
msg.content.splice(firstToolUseIdx, 0, {
type: "thinking",
thinking: cached,
});
}
recordReplay();
continue;
}
}
if (isKimiCoding) {
if (thinkingBlock) {
thinkingBlock.type = "thinking";
thinkingBlock.thinking = "";
delete thinkingBlock.data;
delete thinkingBlock.signature;
} else {
msg.content.splice(firstToolUseIdx, 0, { type: "thinking", thinking: "" });
}
continue;
}
if (requiresAuthenticReasoning) continue;
// Fallback: inject placeholder (must be non-empty for kimi-coding)
msg.content.splice(firstToolUseIdx, 0, {
type: "thinking",
thinking: NON_ANTHROPIC_THINKING_PLACEHOLDER,

View File

@@ -698,12 +698,12 @@ test("translateRequest does not replay reasoning-only messages for non-DeepSeek
clearReasoningCacheAll();
});
test("translateRequest uses Kimi Coding's empty thinking marker instead of cached replay", () => {
test("translateRequest replays cached reasoning before Kimi Coding's empty fallback", () => {
clearReasoningCacheAll();
cacheReasoningByKey(
"toolu_kimi_claude",
"kimi-coding",
"kimi-for-coding",
"kimi-coding-apikey",
"k3-256k",
"cached thinking for Kimi tool call"
);
@@ -712,7 +712,7 @@ test("translateRequest uses Kimi Coding's empty thinking marker instead of cache
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
"k3-256k",
{
reasoning_effort: "high",
messages: [
@@ -733,24 +733,23 @@ test("translateRequest uses Kimi Coding's empty thinking marker instead of cache
},
false,
null,
"kimi-coding"
"kimi-coding-apikey"
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
assert.ok(assistantMsg, "assistant message should exist");
assert.ok(Array.isArray(assistantMsg.content), "content should be array");
// Kimi Code CLI 0.26 sends an explicit empty thinking marker before tool_use.
const thinkingBlock = assistantMsg.content.find((b) => b?.type === "thinking");
assert.ok(thinkingBlock, "thinking block should be injected");
assert.equal(thinkingBlock.thinking, "");
assert.equal(thinkingBlock.thinking, "cached thinking for Kimi tool call");
// Thinking block should appear before tool_use
const thinkingIdx = assistantMsg.content.indexOf(thinkingBlock);
const toolUseIdx = assistantMsg.content.findIndex((b) => b?.type === "tool_use");
assert.ok(thinkingIdx < toolUseIdx, "thinking block should be before tool_use");
assert.equal(getReasoningCacheServiceStats().replays, 0);
assert.equal(getReasoningCacheServiceStats().replays, 1);
clearReasoningCacheAll();
});
@@ -760,7 +759,7 @@ test("translateRequest uses an empty Kimi Coding thinking marker on cache miss",
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
"k3-256k",
{
reasoning_effort: "high",
messages: [
@@ -774,7 +773,7 @@ test("translateRequest uses an empty Kimi Coding thinking marker on cache miss",
},
false,
null,
"kimi-coding"
"kimi-coding-apikey"
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
@@ -790,11 +789,17 @@ test("translateRequest uses an empty Kimi Coding thinking marker on cache miss",
test("translateRequest does NOT inject duplicate thinking for Claude-format messages with existing thinking block", () => {
clearReasoningCacheAll();
cacheReasoningByKey(
"toolu_existing",
"kimi-coding-apikey",
"k3-256k",
"cached thinking must not replace client thinking"
);
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
"k3-256k",
{
messages: [
{ role: "user", content: "hi" },
@@ -810,7 +815,7 @@ test("translateRequest does NOT inject duplicate thinking for Claude-format mess
},
false,
null,
"kimi-coding"
"kimi-coding-apikey"
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
@@ -823,6 +828,7 @@ test("translateRequest does NOT inject duplicate thinking for Claude-format mess
"I already have this",
"original thinking should be preserved"
);
assert.equal(getReasoningCacheServiceStats().replays, 0);
clearReasoningCacheAll();
});