mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
fix(chatCore): only preserve tool_result blocks for Claude-native targets (#13972)
* fix(chatCore): only preserve tool_result blocks for Claude-native targets When forwarding requests through the CC-bridge or Claude passthrough paths to OpenAI-compatible upstream targets (e.g. logfare, OpenRouter), Anthropic-shape tool_result content parts in user messages were preserved unchanged, causing upstream gateways to reject the request with HTTP 503. Gate preserveToolResultBlocks on targetFormat === FORMATS.CLAUDE so that OpenAI targets receive properly normalized content strings. Fixes #13971 * fix(chatCore): add regression coverage for the CC-bridge tool_result guard Adds a one-line clarifying comment on the isClaudePassthrough call site (the guard is a no-op there by construction) and two chatcore-translation-paths tests proving the CC-bridge branch strips raw tool_result blocks for OpenAI-compatible targets while still preserving them for Claude-native ones. --------- Co-authored-by: phs1997 <phs1997@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -2403,7 +2403,12 @@ export async function handleChatCore({
|
||||
extractSystemRoleMessages(translatedBody);
|
||||
} else {
|
||||
// Non-CC path: full normalization including content type conversion.
|
||||
normalizeClaudeUpstreamMessages(translatedBody, { preserveToolResultBlocks: true });
|
||||
// Preserve tool_result blocks only when the upstream target speaks the
|
||||
// Anthropic Messages format — OpenAI-compatible gateways reject them
|
||||
// and return 503. See issue #13971.
|
||||
normalizeClaudeUpstreamMessages(translatedBody, {
|
||||
preserveToolResultBlocks: targetFormat === FORMATS.CLAUDE,
|
||||
});
|
||||
}
|
||||
} else if (isClaudePassthrough) {
|
||||
// Pure passthrough: forward the body as-is without OpenAI round-trip.
|
||||
@@ -2454,7 +2459,16 @@ export async function handleChatCore({
|
||||
ensureCacheControlOnLastUserMessage(translatedBody);
|
||||
}
|
||||
} else {
|
||||
normalizeClaudeUpstreamMessages(translatedBody, { preserveToolResultBlocks: true });
|
||||
// Same guard as the CC-bridge path: only preserve tool_result blocks
|
||||
// for Anthropic-native targets. See issue #13971. This branch only runs
|
||||
// under isClaudePassthrough (sourceFormat === targetFormat === CLAUDE,
|
||||
// defined above), so targetFormat === FORMATS.CLAUDE always holds here —
|
||||
// the guard is a no-op on this call site, kept for symmetry with the
|
||||
// CC-bridge one above rather than a change to code the issue said not
|
||||
// to touch.
|
||||
normalizeClaudeUpstreamMessages(translatedBody, {
|
||||
preserveToolResultBlocks: targetFormat === FORMATS.CLAUDE,
|
||||
});
|
||||
}
|
||||
|
||||
log?.debug?.("FORMAT", `claude passthrough (preserveCache=${preserveCacheControl})`);
|
||||
|
||||
@@ -379,6 +379,7 @@ async function invokeChatCore({
|
||||
reasoningTransportFallback = "drop",
|
||||
managedLease = null,
|
||||
cachedSettings = null,
|
||||
modelTargetFormat = undefined,
|
||||
}: any = {}) {
|
||||
const calls: any[] = [];
|
||||
|
||||
@@ -408,7 +409,10 @@ async function invokeChatCore({
|
||||
const requestBody = structuredClone(body);
|
||||
const result = await handleChatCore({
|
||||
body: requestBody,
|
||||
modelInfo: { provider, model, extendedContext: false },
|
||||
modelInfo:
|
||||
modelTargetFormat !== undefined
|
||||
? { provider, model, extendedContext: false, targetFormat: modelTargetFormat }
|
||||
: { provider, model, extendedContext: false },
|
||||
credentials: credentials || {
|
||||
apiKey: "sk-test",
|
||||
// #13452/#13798: buildUrl() refuses an `*-compatible-*` node with no baseUrl
|
||||
@@ -1565,6 +1569,65 @@ test("chatCore normalizes native Claude Code messages before CC-compatible relay
|
||||
// user msg[2] (was clientMessages[3]): tool_result preserved (preserveToolResultBlocks:true)
|
||||
assert.equal(call.body.messages[2].content[0].type, "tool_result");
|
||||
});
|
||||
|
||||
// Issue #13971: the CC-bridge unconditionally preserved raw tool_result blocks even when the
|
||||
// target speaks OpenAI-compatible (503 on those gateways). Fix: gate preserveToolResultBlocks
|
||||
// on targetFormat === FORMATS.CLAUDE. userAgent is plain (non-Claude-Code) so both requests hit
|
||||
// the CC-bridge's normalizeClaudeUpstreamMessages branch (chatCore.ts:2377-2385), not the
|
||||
// Claude-Code semantic-passthrough branch above it, which this fix does not touch.
|
||||
function ccBridgeToolResultCall(modelTargetFormat?: string) {
|
||||
return invokeChatCore({
|
||||
provider: "anthropic-compatible-cc-test",
|
||||
model: "claude-sonnet-4-6",
|
||||
endpoint: "/v1/messages",
|
||||
credentials: {
|
||||
apiKey: "sk-test",
|
||||
providerSpecificData: { baseUrl: "https://proxy.example.com/v1/messages" },
|
||||
},
|
||||
body: {
|
||||
model: "claude-sonnet-4-6",
|
||||
max_tokens: 64,
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "toolu_x", name: "Read", input: {} }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "toolu_x", content: "file contents" }],
|
||||
},
|
||||
],
|
||||
tools: [{ name: "Read", input_schema: { type: "object", properties: {} } }],
|
||||
},
|
||||
userAgent: "unit-test",
|
||||
responseFormat: "claude",
|
||||
modelTargetFormat,
|
||||
});
|
||||
}
|
||||
test("chatCore strips tool_result blocks on the CC-bridge path when the target is OpenAI-compatible", async () => {
|
||||
const { call, result } = await ccBridgeToolResultCall("openai");
|
||||
assert.equal(result.success, true);
|
||||
// No block may be raw tool_result/tool_use — that shape 503'd on #13971; the
|
||||
// orphan-tool-use cleanup also drops the now-unmatched assistant turn, a stronger guard.
|
||||
for (const message of call.body.messages) {
|
||||
for (const block of message.content) {
|
||||
assert.notEqual(block.type, "tool_result");
|
||||
assert.notEqual(block.type, "tool_use");
|
||||
}
|
||||
}
|
||||
const flattened = call.body.messages
|
||||
.flatMap((m: { content: Array<{ text?: string }> }) => m.content)
|
||||
.map((b: { text?: string }) => b.text)
|
||||
.join("\n");
|
||||
assert.match(flattened, /file contents/);
|
||||
});
|
||||
// Same branch, real (Claude-native) target format — tool_result stays preserved raw.
|
||||
test("chatCore still preserves tool_result blocks on the CC-bridge path when the target is Claude-native", async () => {
|
||||
const { call, result } = await ccBridgeToolResultCall();
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.body.messages[0].content[0].type, "tool_use");
|
||||
assert.equal(call.body.messages[1].content[0].type, "tool_result");
|
||||
});
|
||||
test("chatCore preserves cache_control automatically for Claude Code single-model requests", async () => {
|
||||
await settingsDb.updateSettings({ alwaysPreserveClientCache: "auto" });
|
||||
invalidateCacheControlSettingsCache();
|
||||
|
||||
Reference in New Issue
Block a user