mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(translator): inject thinking placeholder for all Claude-shape upstreams (#2161)
Integrated into release/v3.8.0 — removes redundant provider guard in prepareClaudeRequest, fixing thinking placeholder injection for all Claude-shape upstreams (kimi-coding, glmt, zai).
This commit is contained in:
@@ -250,34 +250,50 @@ export function prepareClaudeRequest(
|
||||
lastAssistantProcessed = true;
|
||||
}
|
||||
|
||||
// Handle thinking blocks for Anthropic endpoints (native + compatible)
|
||||
if (provider === "claude" || provider?.startsWith?.("anthropic-compatible-")) {
|
||||
let hasToolUse = false;
|
||||
let hasThinking = false;
|
||||
// Handle thinking blocks for Anthropic-shape endpoints.
|
||||
// prepareClaudeRequest is only invoked when targetFormat === claude
|
||||
// (translator/index.ts:165-168), so any provider that lands here has
|
||||
// a Claude-format upstream: claude native, anthropic-compatible-*,
|
||||
// kimi-coding (api.kimi.com/coding/v1/messages), glmt, zai, etc.
|
||||
// All of these enforce the same body-shape contract for thinking mode:
|
||||
// when body.thinking.type === "enabled" and an assistant turn contains
|
||||
// a tool_use, the same content[] must include a thinking (or
|
||||
// redacted_thinking) block emitted before the tool_use. Without it,
|
||||
// the upstream rejects with errors like:
|
||||
// "thinking is enabled but reasoning_content is missing in
|
||||
// assistant tool call message at index N" (kimi-coding)
|
||||
// "Invalid signature in thinking block" (claude native, on
|
||||
// cross-provider replay)
|
||||
let hasToolUse = false;
|
||||
let hasThinking = false;
|
||||
|
||||
// Convert thinking blocks to redacted_thinking and replace signature.
|
||||
// When requests cross provider boundaries (e.g., combo fallback), the
|
||||
// original thinking signature is invalid for the new provider, causing
|
||||
// "Invalid signature in thinking block" 400 errors. redacted_thinking
|
||||
// blocks are accepted without signature validation.
|
||||
for (const block of content) {
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
block.type = "redacted_thinking";
|
||||
block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE;
|
||||
delete block.thinking;
|
||||
hasThinking = true;
|
||||
}
|
||||
if (block.type === "tool_use") hasToolUse = true;
|
||||
// Convert thinking blocks to redacted_thinking and replace signature.
|
||||
// When requests cross provider boundaries (e.g., combo fallback), the
|
||||
// original thinking signature is invalid for the new provider, causing
|
||||
// "Invalid signature in thinking block" 400 errors. redacted_thinking
|
||||
// blocks are accepted without signature validation.
|
||||
for (const block of content) {
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
block.type = "redacted_thinking";
|
||||
block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE;
|
||||
delete block.thinking;
|
||||
hasThinking = true;
|
||||
}
|
||||
if (block.type === "tool_use") hasToolUse = true;
|
||||
}
|
||||
|
||||
// Add thinking block if thinking enabled + has tool_use but no thinking
|
||||
if (thinkingEnabled && !hasThinking && hasToolUse) {
|
||||
content.unshift({
|
||||
type: "thinking",
|
||||
thinking: ".",
|
||||
signature: DEFAULT_THINKING_CLAUDE_SIGNATURE,
|
||||
});
|
||||
}
|
||||
// Add thinking block if thinking enabled + has tool_use but no thinking.
|
||||
// Required for Anthropic-shape thinking-mode upstreams (claude, kimi,
|
||||
// glm) when the assistant turn's content[] needs a precursor thinking
|
||||
// block in front of any tool_use. Placeholder content (".") is enough
|
||||
// to satisfy shape validation; the upstream still runs its own
|
||||
// reasoning on the current turn.
|
||||
if (thinkingEnabled && !hasThinking && hasToolUse) {
|
||||
content.unshift({
|
||||
type: "thinking",
|
||||
thinking: ".",
|
||||
signature: DEFAULT_THINKING_CLAUDE_SIGNATURE,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
128
tests/unit/translator-claude-helper-thinking.test.ts
Normal file
128
tests/unit/translator-claude-helper-thinking.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { prepareClaudeRequest } = await import(
|
||||
"../../open-sse/translator/helpers/claudeHelper.ts"
|
||||
);
|
||||
const { DEFAULT_THINKING_CLAUDE_SIGNATURE } = await import(
|
||||
"../../open-sse/config/defaultThinkingSignature.ts"
|
||||
);
|
||||
|
||||
function multiTurnBodyWithoutThinkingBlock() {
|
||||
return {
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
messages: [
|
||||
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "call_x", name: "ls", input: { path: "." } },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "call_x",
|
||||
content: "README.md\npackage.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test("prepareClaudeRequest: claude provider — injects thinking before tool_use (regression)", () => {
|
||||
const body = multiTurnBodyWithoutThinkingBlock();
|
||||
const result = prepareClaudeRequest(body as any, "claude");
|
||||
const assistantContent = (result as any).messages[1].content;
|
||||
assert.equal(assistantContent.length, 2, "thinking + tool_use");
|
||||
assert.equal(assistantContent[0].type, "thinking");
|
||||
assert.equal(assistantContent[0].thinking, ".");
|
||||
assert.equal(assistantContent[0].signature, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
assert.equal(assistantContent[1].type, "tool_use");
|
||||
});
|
||||
|
||||
test("prepareClaudeRequest: kimi-coding provider — injects thinking before tool_use (new behavior)", () => {
|
||||
// Previously: kimi-coding was excluded by the provider gate and the assistant
|
||||
// turn shipped to api.kimi.com/coding/v1/messages without a thinking
|
||||
// precursor, triggering 400 "thinking is enabled but reasoning_content is
|
||||
// missing in assistant tool call message at index N".
|
||||
const body = multiTurnBodyWithoutThinkingBlock();
|
||||
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
||||
const assistantContent = (result as any).messages[1].content;
|
||||
assert.equal(assistantContent.length, 2, "thinking + tool_use");
|
||||
assert.equal(assistantContent[0].type, "thinking");
|
||||
assert.equal(assistantContent[0].thinking, ".");
|
||||
assert.equal(assistantContent[0].signature, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
});
|
||||
|
||||
test("prepareClaudeRequest: existing thinking block — redacted, signature replaced, no double-inject", () => {
|
||||
const body = {
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
messages: [
|
||||
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "reasoning here", signature: "old-sig" },
|
||||
{ type: "tool_use", id: "call_y", name: "ls", input: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "call_y", content: "ok" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
||||
const assistantContent = (result as any).messages[1].content;
|
||||
assert.equal(assistantContent.length, 2, "no double-inject — exactly 2 blocks");
|
||||
assert.equal(assistantContent[0].type, "redacted_thinking", "thinking → redacted_thinking");
|
||||
assert.equal(assistantContent[0].signature, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
assert.equal(assistantContent[0].thinking, undefined, "thinking field stripped");
|
||||
assert.equal(assistantContent[1].type, "tool_use");
|
||||
});
|
||||
|
||||
test("prepareClaudeRequest: thinking disabled — no inject regardless of tool_use presence", () => {
|
||||
const body = {
|
||||
thinking: { type: "disabled" },
|
||||
messages: [
|
||||
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "call_z", name: "ls", input: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "call_z", content: "ok" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
||||
const assistantContent = (result as any).messages[1].content;
|
||||
assert.equal(assistantContent.length, 1, "no thinking injected when thinking is disabled");
|
||||
assert.equal(assistantContent[0].type, "tool_use");
|
||||
});
|
||||
|
||||
test("prepareClaudeRequest: thinking enabled + no tool_use — no inject (single-turn text)", () => {
|
||||
const body = {
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
messages: [
|
||||
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
||||
],
|
||||
};
|
||||
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
||||
const userContent = (result as any).messages[0].content;
|
||||
assert.ok(Array.isArray(userContent));
|
||||
assert.equal(userContent[0].type, "text");
|
||||
// No new thinking block prepended on user messages
|
||||
assert.equal(userContent.length, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user