mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(thinking): only inject redacted_thinking replay block when tool_use present and thinking enabled (#5945) (#5953)
Integrated into release/v3.8.44.
This commit is contained in:
committed by
GitHub
parent
8d2df914f0
commit
2b0da37c19
@@ -165,6 +165,97 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
result.stop_sequences = Array.isArray(body.stop) ? body.stop : [body.stop];
|
||||
}
|
||||
|
||||
// Thinking configuration
|
||||
// NOTE: computed BEFORE message-block conversion (below) so that
|
||||
// `getContentBlocksFromMessage` knows whether the outbound request actually has
|
||||
// extended thinking enabled — required to correctly gate the `redacted_thinking`
|
||||
// replay-placeholder injection (#5945). This block has no dependency on
|
||||
// `result.messages`/`toolNameMap`, so moving it earlier is safe.
|
||||
if (body.thinking) {
|
||||
result.thinking = {
|
||||
type: body.thinking.type || "enabled",
|
||||
...(body.thinking.budget_tokens && { budget_tokens: body.thinking.budget_tokens }),
|
||||
...(body.thinking.max_tokens && { max_tokens: body.thinking.max_tokens }),
|
||||
};
|
||||
} else if (body.reasoning_effort) {
|
||||
// Convert OpenAI reasoning_effort to Claude thinking format (#627)
|
||||
// Clients like OpenCode send reasoning_effort via @ai-sdk/openai-compatible
|
||||
const requestedEffort = String(body.reasoning_effort).toLowerCase();
|
||||
const normalizedEffort =
|
||||
requestedEffort === "max" && !supportsClaudeMaxEffort(model)
|
||||
? "high"
|
||||
: requestedEffort === "xhigh" && !supportsXHighEffort("claude", model)
|
||||
? "high"
|
||||
: requestedEffort;
|
||||
if (isAdaptiveThinkingOnly(model)) {
|
||||
// Opus 4.7+/Fable 5 removed manual extended thinking: a fixed `budget_tokens`
|
||||
// (or `type:"enabled"`) is a hard 400. Steer EVERY level via adaptive +
|
||||
// output_config.effort instead of the budget buckets below. Unrecognized levels
|
||||
// leave thinking unset so the model keeps its adaptive default rather than 400ing
|
||||
// on an invalid effort value.
|
||||
if (ADAPTIVE_EFFORT_LEVELS.has(normalizedEffort)) {
|
||||
result.thinking = {
|
||||
type: "adaptive",
|
||||
};
|
||||
result.output_config = {
|
||||
...(result.output_config || {}),
|
||||
effort: normalizedEffort,
|
||||
};
|
||||
}
|
||||
} else if (normalizedEffort === "max" || normalizedEffort === "xhigh") {
|
||||
result.thinking = {
|
||||
type: "adaptive",
|
||||
};
|
||||
result.output_config = {
|
||||
...(result.output_config || {}),
|
||||
effort: normalizedEffort,
|
||||
};
|
||||
} else {
|
||||
const effortBudgetMap: Record<string, number> = {
|
||||
low: 1024,
|
||||
medium: 10240,
|
||||
high: 131072,
|
||||
max: 131072,
|
||||
};
|
||||
const budget = effortBudgetMap[normalizedEffort];
|
||||
if (budget !== undefined && budget > 0) {
|
||||
result.thinking = {
|
||||
type: "enabled",
|
||||
budget_tokens: budget,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fit thinking budget within the model's output cap and ensure
|
||||
// max_tokens > budget_tokens for all thinking configurations (#627).
|
||||
// Replaces the previous unconditional `budget + 8192` inflation, which
|
||||
// could exceed model caps (e.g. Opus 4.7's 128000 ceiling) and trigger
|
||||
// HTTP 400 from Anthropic.
|
||||
const fitted = fitThinkingToMaxTokens(model, Number(result.max_tokens) || 0, result.thinking);
|
||||
result.max_tokens = fitted.maxTokens;
|
||||
if (fitted.thinking === undefined) {
|
||||
delete result.thinking;
|
||||
} else {
|
||||
result.thinking = applyCopilotSummarizedThinkingDisplay(fitted.thinking, body);
|
||||
}
|
||||
|
||||
delete result[COPILOT_REASONING_SUMMARY_MARKER];
|
||||
|
||||
// Final guard: Claude rejects `temperature` whenever extended thinking is
|
||||
// enabled. If `result.thinking` was set above from `body.thinking` or
|
||||
// `body.reasoning_effort` (manual budget or adaptive effort), drop temperature
|
||||
// defensively. The model-name strip earlier already covers Claude OAuth's
|
||||
// forced-thinking case (claude-opus-4.x / claude-sonnet-4.x).
|
||||
if (result.thinking && result.temperature !== undefined) {
|
||||
delete result.temperature;
|
||||
}
|
||||
|
||||
// Whether the OUTBOUND request actually has extended thinking enabled. Anthropic's
|
||||
// schema only requires a precursor thinking/redacted_thinking block before a tool_use
|
||||
// block when thinking mode is active for THIS request — never unconditionally (#5945).
|
||||
const thinkingEnabledForRequest = Boolean(result.thinking) && result.thinking.type !== "disabled";
|
||||
|
||||
// Messages
|
||||
const systemParts = [];
|
||||
|
||||
@@ -199,7 +290,12 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
|
||||
for (const msg of nonSystemMessages) {
|
||||
const newRole = msg.role === "user" || msg.role === "tool" ? "user" : "assistant";
|
||||
const blocks = getContentBlocksFromMessage(msg, toolNameMap, disableToolPrefix);
|
||||
const blocks = getContentBlocksFromMessage(
|
||||
msg,
|
||||
toolNameMap,
|
||||
disableToolPrefix,
|
||||
thinkingEnabledForRequest
|
||||
);
|
||||
const hasToolUse = blocks.some((b) => b.type === "tool_use");
|
||||
const hasToolResult = blocks.some((b) => b.type === "tool_result");
|
||||
|
||||
@@ -387,87 +483,6 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
: [{ type: "text", text: String(body.system) }];
|
||||
}
|
||||
|
||||
// Thinking configuration
|
||||
if (body.thinking) {
|
||||
result.thinking = {
|
||||
type: body.thinking.type || "enabled",
|
||||
...(body.thinking.budget_tokens && { budget_tokens: body.thinking.budget_tokens }),
|
||||
...(body.thinking.max_tokens && { max_tokens: body.thinking.max_tokens }),
|
||||
};
|
||||
} else if (body.reasoning_effort) {
|
||||
// Convert OpenAI reasoning_effort to Claude thinking format (#627)
|
||||
// Clients like OpenCode send reasoning_effort via @ai-sdk/openai-compatible
|
||||
const requestedEffort = String(body.reasoning_effort).toLowerCase();
|
||||
const normalizedEffort =
|
||||
requestedEffort === "max" && !supportsClaudeMaxEffort(model)
|
||||
? "high"
|
||||
: requestedEffort === "xhigh" && !supportsXHighEffort("claude", model)
|
||||
? "high"
|
||||
: requestedEffort;
|
||||
if (isAdaptiveThinkingOnly(model)) {
|
||||
// Opus 4.7+/Fable 5 removed manual extended thinking: a fixed `budget_tokens`
|
||||
// (or `type:"enabled"`) is a hard 400. Steer EVERY level via adaptive +
|
||||
// output_config.effort instead of the budget buckets below. Unrecognized levels
|
||||
// leave thinking unset so the model keeps its adaptive default rather than 400ing
|
||||
// on an invalid effort value.
|
||||
if (ADAPTIVE_EFFORT_LEVELS.has(normalizedEffort)) {
|
||||
result.thinking = {
|
||||
type: "adaptive",
|
||||
};
|
||||
result.output_config = {
|
||||
...(result.output_config || {}),
|
||||
effort: normalizedEffort,
|
||||
};
|
||||
}
|
||||
} else if (normalizedEffort === "max" || normalizedEffort === "xhigh") {
|
||||
result.thinking = {
|
||||
type: "adaptive",
|
||||
};
|
||||
result.output_config = {
|
||||
...(result.output_config || {}),
|
||||
effort: normalizedEffort,
|
||||
};
|
||||
} else {
|
||||
const effortBudgetMap: Record<string, number> = {
|
||||
low: 1024,
|
||||
medium: 10240,
|
||||
high: 131072,
|
||||
max: 131072,
|
||||
};
|
||||
const budget = effortBudgetMap[normalizedEffort];
|
||||
if (budget !== undefined && budget > 0) {
|
||||
result.thinking = {
|
||||
type: "enabled",
|
||||
budget_tokens: budget,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fit thinking budget within the model's output cap and ensure
|
||||
// max_tokens > budget_tokens for all thinking configurations (#627).
|
||||
// Replaces the previous unconditional `budget + 8192` inflation, which
|
||||
// could exceed model caps (e.g. Opus 4.7's 128000 ceiling) and trigger
|
||||
// HTTP 400 from Anthropic.
|
||||
const fitted = fitThinkingToMaxTokens(model, Number(result.max_tokens) || 0, result.thinking);
|
||||
result.max_tokens = fitted.maxTokens;
|
||||
if (fitted.thinking === undefined) {
|
||||
delete result.thinking;
|
||||
} else {
|
||||
result.thinking = applyCopilotSummarizedThinkingDisplay(fitted.thinking, body);
|
||||
}
|
||||
|
||||
delete result[COPILOT_REASONING_SUMMARY_MARKER];
|
||||
|
||||
// Final guard: Claude rejects `temperature` whenever extended thinking is
|
||||
// enabled. If `result.thinking` was set above from `body.thinking` or
|
||||
// `body.reasoning_effort` (manual budget or adaptive effort), drop temperature
|
||||
// defensively. The model-name strip earlier already covers Claude OAuth's
|
||||
// forced-thinking case (claude-opus-4.x / claude-sonnet-4.x).
|
||||
if (result.thinking && result.temperature !== undefined) {
|
||||
delete result.temperature;
|
||||
}
|
||||
|
||||
// Attach toolNameMap to result for response translation
|
||||
if (toolNameMap.size > 0) {
|
||||
result._toolNameMap = toolNameMap;
|
||||
@@ -488,7 +503,12 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
}
|
||||
|
||||
// Get content blocks from single message
|
||||
function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPrefix = false) {
|
||||
function getContentBlocksFromMessage(
|
||||
msg,
|
||||
toolNameMap = new Map(),
|
||||
disableToolPrefix = false,
|
||||
thinkingEnabledForRequest = false
|
||||
) {
|
||||
const blocks = [];
|
||||
|
||||
if (msg.role === "tool") {
|
||||
@@ -555,22 +575,6 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPr
|
||||
}
|
||||
}
|
||||
} else if (msg.role === "assistant") {
|
||||
// Add reasoning_content as a replay placeholder (OpenAI extended thinking format).
|
||||
// #5312 RC-D: reasoning_content carries NO real Claude signature. Emitting a
|
||||
// `thinking` block with the fabricated DEFAULT signature makes Anthropic reject the
|
||||
// replay with 400 "Invalid signature in thinking block" — and claudeHelper's
|
||||
// latest-assistant guard (prepareClaudeRequest) preserves it verbatim, so the fake
|
||||
// signature leaks upstream. Emit a signature-less redacted_thinking block instead
|
||||
// (the same shape prepareClaudeRequest produces for Anthropic-native replay);
|
||||
// Anthropic accepts it without signature validation and non-Anthropic Claude-shape
|
||||
// upstreams re-hydrate the real text downstream from reasoningCache.
|
||||
if (msg.reasoning_content) {
|
||||
blocks.push({
|
||||
type: "redacted_thinking",
|
||||
data: DEFAULT_THINKING_CLAUDE_SIGNATURE,
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
@@ -619,6 +623,37 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add reasoning_content as a replay placeholder (OpenAI extended thinking format) —
|
||||
// ONLY when Anthropic's schema actually requires a precursor thinking block: the
|
||||
// outbound request has extended thinking enabled AND this assistant turn contains a
|
||||
// tool_use block (Anthropic rejects a tool_use turn without a preceding
|
||||
// thinking/redacted_thinking block when thinking is active). #5312 RC-D:
|
||||
// reasoning_content carries NO real Claude signature. Emitting a `thinking` block
|
||||
// with the fabricated DEFAULT signature makes Anthropic reject the replay with 400
|
||||
// "Invalid signature in thinking block" — and claudeHelper's latest-assistant guard
|
||||
// (prepareClaudeRequest) preserves it verbatim, so the fake signature leaks
|
||||
// upstream. Emit a signature-less redacted_thinking block instead (the same shape
|
||||
// prepareClaudeRequest produces for Anthropic-native replay, gated the same way at
|
||||
// claudeHelper.ts `thinkingEnabled && !hasThinking && hasToolUse`); Anthropic
|
||||
// accepts it without signature validation and non-Anthropic Claude-shape upstreams
|
||||
// re-hydrate the real text downstream from reasoningCache.
|
||||
// #5945: injecting this unconditionally — for ANY assistant turn carrying
|
||||
// reasoning_content, regardless of tool_use or thinking state — fabricates a content
|
||||
// block the client never sent. Some upstream clients (reported: Claude Sonnet 5 via
|
||||
// the "Pi" harness) detect the extra block and refuse the turn as prompt injection.
|
||||
// Drop reasoning_content silently when it is not required by the schema, mirroring
|
||||
// how other echo-only fields are dropped (see OPENAI_INCOMPATIBLE_ECHO_FIELDS).
|
||||
const hasThinkingBlock = blocks.some(
|
||||
(b) => b.type === "thinking" || b.type === "redacted_thinking"
|
||||
);
|
||||
const hasToolUseBlock = blocks.some((b) => b.type === "tool_use");
|
||||
if (msg.reasoning_content && thinkingEnabledForRequest && hasToolUseBlock && !hasThinkingBlock) {
|
||||
blocks.unshift({
|
||||
type: "redacted_thinking",
|
||||
data: DEFAULT_THINKING_CLAUDE_SIGNATURE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
/**
|
||||
* TDD regression for #5312 (FIX D / RC-D): openai-to-claude reconstructed a Claude
|
||||
* `thinking` block from signature-less `reasoning_content` and stamped it with the
|
||||
* fabricated DEFAULT_THINKING_CLAUDE_SIGNATURE. Anthropic validates signatures and
|
||||
* rejects the fake one with 400 "Invalid signature in thinking block" — and
|
||||
* claudeHelper's latest-assistant guard preserves the block verbatim, so the fake
|
||||
* signature leaks upstream.
|
||||
* TDD regression for #5312 (FIX D / RC-D) and #5945.
|
||||
*
|
||||
* Fix: emit a signature-less `redacted_thinking` placeholder (matching what
|
||||
* prepareClaudeRequest produces downstream). A REAL part.signature must always be
|
||||
* preserved verbatim — never overwritten with the default.
|
||||
* #5312 (FIX D / RC-D): openai-to-claude reconstructed a Claude `thinking` block from
|
||||
* signature-less `reasoning_content` and stamped it with the fabricated
|
||||
* DEFAULT_THINKING_CLAUDE_SIGNATURE. Anthropic validates signatures and rejects the
|
||||
* fake one with 400 "Invalid signature in thinking block" — and claudeHelper's
|
||||
* latest-assistant guard preserves the block verbatim, so the fake signature leaks
|
||||
* upstream.
|
||||
*
|
||||
* Fix (#5312): when a precursor thinking block IS required by Anthropic's schema
|
||||
* (assistant turn has tool_use AND the outbound request has extended thinking
|
||||
* enabled), emit a signature-less `redacted_thinking` placeholder (matching what
|
||||
* prepareClaudeRequest produces downstream) instead of a fabricated-signature
|
||||
* `thinking` block. A REAL part.signature must always be preserved verbatim — never
|
||||
* overwritten with the default.
|
||||
*
|
||||
* #5945 (over-correction of #5312): the original #5312 fix injected the
|
||||
* redacted_thinking placeholder UNCONDITIONALLY whenever ANY assistant history
|
||||
* message carried non-empty `reasoning_content` — regardless of whether the current
|
||||
* outbound request has thinking enabled, and regardless of whether that assistant
|
||||
* turn even contains a `tool_use` block (the only case Anthropic's schema actually
|
||||
* requires a preceding thinking/redacted_thinking block). This fabricated a content
|
||||
* block the client never sent; reported by dev-cj: Claude Sonnet 5 via the "Pi"
|
||||
* harness detected the extra block and refused the turn as prompt injection.
|
||||
*
|
||||
* Fix (#5945): gate the injection on BOTH (a) the assistant turn containing a
|
||||
* tool_use block and (b) the outbound request having extended thinking enabled.
|
||||
* Otherwise `reasoning_content` is dropped silently — it carries no useful signal
|
||||
* for a plain-text replay turn, and the client never asked for it to appear.
|
||||
*
|
||||
* These two fixes are not in tension: #5312 legitimately fixed a real Anthropic 400
|
||||
* for the case the redacted_thinking block IS required; #5945 narrows the trigger to
|
||||
* exactly that case instead of firing for every reasoning_content-bearing message.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -20,7 +43,7 @@ const { DEFAULT_THINKING_CLAUDE_SIGNATURE } = await import(
|
||||
"../../open-sse/config/defaultThinkingSignature.ts"
|
||||
);
|
||||
|
||||
test("#5312 RC-D: signature-less reasoning_content yields no fabricated-signature thinking block", () => {
|
||||
test("#5945: reasoning_content on a plain-text assistant turn (no tool_use, thinking not requested) yields NO redacted_thinking/thinking block", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-4-8",
|
||||
{
|
||||
@@ -28,6 +51,83 @@ test("#5312 RC-D: signature-less reasoning_content yields no fabricated-signatur
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", reasoning_content: "thinking about it", content: "hi there" },
|
||||
],
|
||||
// no body.thinking / body.reasoning_effort — thinking is NOT enabled for this request.
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const assistant = result.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "expected assistant message");
|
||||
|
||||
// No fabricated block at all — reasoning_content must be dropped silently, exactly
|
||||
// like OPENAI_INCOMPATIBLE_ECHO_FIELDS drops other echo-only fields.
|
||||
assert.equal(
|
||||
assistant.content.find((b) => b && (b.type === "thinking" || b.type === "redacted_thinking")),
|
||||
undefined,
|
||||
"must NOT fabricate a thinking/redacted_thinking block the client never sent"
|
||||
);
|
||||
assert.deepEqual(
|
||||
assistant.content.map((b) => b.type),
|
||||
["text"],
|
||||
"assistant content should contain only the real text block"
|
||||
);
|
||||
});
|
||||
|
||||
test("#5945: reasoning_content + tool_use, but thinking NOT enabled on the outbound request, yields NO injection", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-4-8",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
reasoning_content: "thinking about it",
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: "{}" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
// no body.thinking / body.reasoning_effort — thinking is NOT enabled.
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const assistant = result.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "expected assistant message");
|
||||
assert.equal(
|
||||
assistant.content.find((b) => b && (b.type === "thinking" || b.type === "redacted_thinking")),
|
||||
undefined,
|
||||
"must NOT inject a precursor thinking block when the request itself has thinking disabled"
|
||||
);
|
||||
assert.ok(
|
||||
assistant.content.some((b) => b.type === "tool_use"),
|
||||
"tool_use block must still be present"
|
||||
);
|
||||
});
|
||||
|
||||
test("#5312: reasoning_content + tool_use + thinking ENABLED still gets a signature-less redacted_thinking precursor (the legitimate #5312 400-fix case)", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-4-8",
|
||||
{
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
reasoning_content: "thinking about it",
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: "{}" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
@@ -35,24 +135,25 @@ test("#5312 RC-D: signature-less reasoning_content yields no fabricated-signatur
|
||||
const assistant = result.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "expected assistant message");
|
||||
|
||||
// No block may carry the fabricated default signature.
|
||||
// No block may carry the fabricated default signature on a `thinking`-typed block.
|
||||
const fake = assistant.content.find(
|
||||
(b) => b && b.signature === DEFAULT_THINKING_CLAUDE_SIGNATURE
|
||||
(b) => b && b.type === "thinking" && b.signature === DEFAULT_THINKING_CLAUDE_SIGNATURE
|
||||
);
|
||||
assert.equal(fake, undefined, "must NOT emit a thinking block with the fabricated signature");
|
||||
assert.equal(fake, undefined, "must NOT emit a `thinking` block with the fabricated signature");
|
||||
|
||||
// No `thinking`-typed block at all from signature-less reasoning_content.
|
||||
// It becomes a redacted_thinking placeholder (Anthropic accepts without sig check),
|
||||
// and it must precede the tool_use block.
|
||||
assert.equal(assistant.content[0].type, "redacted_thinking", "must be the precursor block");
|
||||
assert.equal(assistant.content[0].data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
assert.equal(
|
||||
assistant.content.find((b) => b && b.type === "thinking"),
|
||||
assistant.content[0].signature,
|
||||
undefined,
|
||||
"signature-less reasoning_content must not produce a `thinking` block"
|
||||
"redacted_thinking must not carry a signature"
|
||||
);
|
||||
assert.ok(
|
||||
assistant.content.some((b) => b.type === "tool_use"),
|
||||
"tool_use block must still be present"
|
||||
);
|
||||
|
||||
// It becomes a redacted_thinking placeholder (Anthropic accepts without sig check).
|
||||
const redacted = assistant.content.find((b) => b && b.type === "redacted_thinking");
|
||||
assert.ok(redacted, "expected a redacted_thinking placeholder");
|
||||
assert.equal(redacted.data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
assert.equal(redacted.signature, undefined, "redacted_thinking must not carry a signature");
|
||||
});
|
||||
|
||||
test("#5312 RC-D: a REAL thinking signature is preserved verbatim", () => {
|
||||
|
||||
@@ -114,6 +114,11 @@ test("OpenAI -> Claude converts multimodal content, tool declarations, tool call
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-4-sonnet",
|
||||
{
|
||||
// #5945: the redacted_thinking precursor is only emitted when the outbound
|
||||
// request actually has extended thinking enabled (Anthropic's schema
|
||||
// requirement). Set it explicitly so this test keeps exercising that
|
||||
// legitimate #5312 case alongside the multimodal/tool assertions below.
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
|
||||
Reference in New Issue
Block a user