mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
fix(translator): replay cached Kimi reasoning before fallback
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user