From c14e43a52f80e267e7b2170e51cf3d20b4eaa212 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 10 May 2026 15:29:17 -0300 Subject: [PATCH] =?UTF-8?q?fix(translator):=20preserve=20body.system=20in?= =?UTF-8?q?=20openai=E2=86=92claude=20when=20Claude=20Code=20sends=20nativ?= =?UTF-8?q?e=20format=20(#2130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: v3.7.9 fix for #1966 removed the unconditional CLAUDE_SYSTEM_PROMPT injection, which also removed the else branch that always set result.system. When Claude Code sends system prompt as body.system (native Anthropic array) through /v1/chat/completions, the translator only looked at role='system' messages in body.messages — body.system was silently dropped. Fix: The translator now checks for body.system and preserves it: - If both body.system and role='system' messages exist, they are merged - If only body.system exists, it passes through as-is - If only role='system' messages exist, behavior unchanged - If neither exists, result.system remains undefined (no forced injection) Also removes the dead CLAUDE_SYSTEM_PROMPT import. Includes 4 regression tests covering all combinations. --- CHANGELOG.md | 79 +++++++------- .../translator/request/openai-to-claude.ts | 26 ++++- .../unit/fix-2130-system-passthrough.test.ts | 103 ++++++++++++++++++ 3 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 tests/unit/fix-2130-system-passthrough.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d145e0112..875588b7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - **feat(qdrant):** embedding model discovery (#2086) - **feat(auth):** per-session sticky routing for Codex (#1887) - **feat(inworld):** enhance Inworld TTS support (#2123) +- **feat(kiro):** headless auth via kiro-cli SQLite, image support, tool overflow handling, and model list sync (#2129 — thanks @christlau) ### 🐛 Bug Fixes @@ -81,6 +82,7 @@ - **fix:** remove Anthropic-Beta header from non-Anthropic providers to fix identity contamination (#1989) - **fix(cli):** resolve .env loading failure for global npm installations - **fix(export):** exclude telemetry/usage-history tables from JSON config backups by default to prevent unbounded file growth (#2125) +- **fix(translator):** preserve `body.system` in openai→claude translator when Claude Code sends native Anthropic system array through /chat/completions — fixes v3.7.9 regression where system prompt was silently dropped, triggering Anthropic 429 (#2130) - **fix:** Follow OpenAI specification, handle throttling in batch and fix UI (#2045) ### 🔒 Security @@ -109,44 +111,45 @@ Thank you to all **38 community contributors** who made v3.8.0 possible! 🎉 -| Contributor | PRs | Contributions | -| :--- | :---: | :--- | -| [@oyi77](https://github.com/oyi77) | 7 | #2010, #2014, #2041, #2052, #2061, #2074, #2091, #2094, #2096 | -| [@backryun](https://github.com/backryun) | 4 | #1992, #2033, #2088, #2123 | -| [@dhaern](https://github.com/dhaern) | 4 | #2028, #2039, #2087, #2090 | -| [@Tentoxa](https://github.com/Tentoxa) | 2 | #2011, #2053 | -| [@wauputr4](https://github.com/wauputr4) | 2 | #2009, #2046 | -| [@payne0420](https://github.com/payne0420) | 1 | #2082 | -| [@Tr0sT](https://github.com/Tr0sT) | 1 | #2012 | -| [@AveryanAlex](https://github.com/AveryanAlex) | 1 | #2008 | -| [@nickwizard](https://github.com/nickwizard) | 1 | #1991 | -| [@rodrigogbbr-stack](https://github.com/rodrigogbbr-stack) | 1 | #1996 | -| [@NekoMonci12](https://github.com/NekoMonci12) | 1 | #1999 | -| [@congvc-dev](https://github.com/congvc-dev) | 1 | #2004 | -| [@tatsster](https://github.com/tatsster) | 1 | #2007 | -| [@xssdem](https://github.com/xssdem) | 1 | #2023 | -| [@bypanghu](https://github.com/bypanghu) | 1 | #2027 | -| [@herjarsa](https://github.com/herjarsa) | 1 | #2030 | -| [@wucm667](https://github.com/wucm667) | 1 | #2031 | -| [@hartmark](https://github.com/hartmark) | 1 | #2045 | -| [@ddarkr](https://github.com/ddarkr) | 1 | #2047 | -| [@tces1](https://github.com/tces1) | 1 | #2048 | -| [@guanbear](https://github.com/guanbear) | 1 | #2054 | -| [@Gi99lin](https://github.com/Gi99lin) | 1 | #2055 | -| [@ivan-mezentsev](https://github.com/ivan-mezentsev) | 1 | #2063 | -| [@JxnLexn](https://github.com/JxnLexn) | 1 | #2019 | -| [@yoviarpauzi](https://github.com/yoviarpauzi) | 1 | #2092 | -| [@rafacpti23](https://github.com/rafacpti23) | 1 | #2086 | -| [@gleber](https://github.com/gleber) | 1 | #2103 | -| [@rilham97](https://github.com/rilham97) | 1 | #2104 | -| [@Gioxaa](https://github.com/Gioxaa) | 1 | #2105 | -| [@boa-z](https://github.com/boa-z) | 1 | #2115 | -| [@eleata](https://github.com/eleata) | 1 | #2116 | -| [@rdself](https://github.com/rdself) | 1 | #2118 | -| [@clousky2020](https://github.com/clousky2020) | 1 | #2119 | -| [@abhinavjnu](https://github.com/abhinavjnu) | 1 | #2122 | -| [@HoaPham98](https://github.com/HoaPham98) | 1 | #2089 | -| [@05dunski](https://github.com/05dunski) | 1 | #1978 (cherry-picked) | +| Contributor | PRs | Contributions | +| :--------------------------------------------------------- | :-: | :------------------------------------------------------------ | +| [@oyi77](https://github.com/oyi77) | 7 | #2010, #2014, #2041, #2052, #2061, #2074, #2091, #2094, #2096 | +| [@backryun](https://github.com/backryun) | 4 | #1992, #2033, #2088, #2123 | +| [@dhaern](https://github.com/dhaern) | 4 | #2028, #2039, #2087, #2090 | +| [@Tentoxa](https://github.com/Tentoxa) | 2 | #2011, #2053 | +| [@wauputr4](https://github.com/wauputr4) | 2 | #2009, #2046 | +| [@payne0420](https://github.com/payne0420) | 1 | #2082 | +| [@Tr0sT](https://github.com/Tr0sT) | 1 | #2012 | +| [@AveryanAlex](https://github.com/AveryanAlex) | 1 | #2008 | +| [@nickwizard](https://github.com/nickwizard) | 1 | #1991 | +| [@rodrigogbbr-stack](https://github.com/rodrigogbbr-stack) | 1 | #1996 | +| [@NekoMonci12](https://github.com/NekoMonci12) | 1 | #1999 | +| [@congvc-dev](https://github.com/congvc-dev) | 1 | #2004 | +| [@tatsster](https://github.com/tatsster) | 1 | #2007 | +| [@xssdem](https://github.com/xssdem) | 1 | #2023 | +| [@bypanghu](https://github.com/bypanghu) | 1 | #2027 | +| [@herjarsa](https://github.com/herjarsa) | 1 | #2030 | +| [@wucm667](https://github.com/wucm667) | 1 | #2031 | +| [@hartmark](https://github.com/hartmark) | 1 | #2045 | +| [@ddarkr](https://github.com/ddarkr) | 1 | #2047 | +| [@tces1](https://github.com/tces1) | 1 | #2048 | +| [@guanbear](https://github.com/guanbear) | 1 | #2054 | +| [@Gi99lin](https://github.com/Gi99lin) | 1 | #2055 | +| [@ivan-mezentsev](https://github.com/ivan-mezentsev) | 1 | #2063 | +| [@JxnLexn](https://github.com/JxnLexn) | 1 | #2019 | +| [@yoviarpauzi](https://github.com/yoviarpauzi) | 1 | #2092 | +| [@rafacpti23](https://github.com/rafacpti23) | 1 | #2086 | +| [@gleber](https://github.com/gleber) | 1 | #2103 | +| [@rilham97](https://github.com/rilham97) | 1 | #2104 | +| [@Gioxaa](https://github.com/Gioxaa) | 1 | #2105 | +| [@boa-z](https://github.com/boa-z) | 1 | #2115 | +| [@eleata](https://github.com/eleata) | 1 | #2116 | +| [@rdself](https://github.com/rdself) | 1 | #2118 | +| [@clousky2020](https://github.com/clousky2020) | 1 | #2119 | +| [@abhinavjnu](https://github.com/abhinavjnu) | 1 | #2122 | +| [@HoaPham98](https://github.com/HoaPham98) | 1 | #2089 | +| [@christlau](https://github.com/christlau) | 1 | #2129 | +| [@05dunski](https://github.com/05dunski) | 1 | #1978 (cherry-picked) | ## [3.7.9] — 2026-05-03 diff --git a/open-sse/translator/request/openai-to-claude.ts b/open-sse/translator/request/openai-to-claude.ts index 14bccca754..50f2973bb7 100644 --- a/open-sse/translator/request/openai-to-claude.ts +++ b/open-sse/translator/request/openai-to-claude.ts @@ -1,6 +1,6 @@ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; -import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.ts"; +// CLAUDE_SYSTEM_PROMPT import removed — no longer injected unconditionally (#1966/#2130) import { supportsXHighEffort } from "../../config/providerModels.ts"; import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts"; import { sanitizeToolId } from "../helpers/schemaCoercion.ts"; @@ -313,11 +313,29 @@ export function openaiToClaudeRequest(model, body, stream) { } // System messages and cache_control + // Fix #2130: Preserve body.system when present (Claude Code sends system as native + // Anthropic array through the /chat/completions endpoint). Without this, the system + // prompt is silently dropped when no role="system" messages exist in body.messages. if (systemParts.length > 0) { const systemText = systemParts.join("\n"); - result.system = [ - { type: "text", text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }, - ]; + const systemBlock = { + type: "text", + text: systemText, + cache_control: { type: "ephemeral", ttl: "1h" }, + }; + // Merge with existing body.system if present + if (Array.isArray(body.system)) { + result.system = [...body.system, systemBlock]; + } else if (typeof body.system === "string" && body.system.length > 0) { + result.system = [{ type: "text", text: body.system }, systemBlock]; + } else { + result.system = [systemBlock]; + } + } else if (body.system) { + // No role="system" messages, but body.system exists — pass through as-is + result.system = Array.isArray(body.system) + ? body.system + : [{ type: "text", text: String(body.system) }]; } // Thinking configuration diff --git a/tests/unit/fix-2130-system-passthrough.test.ts b/tests/unit/fix-2130-system-passthrough.test.ts new file mode 100644 index 0000000000..f45fe072f8 --- /dev/null +++ b/tests/unit/fix-2130-system-passthrough.test.ts @@ -0,0 +1,103 @@ +/** + * Regression test for #2130: system prompt missing for Claude Code OAuth on Linux. + * + * Claude Code sends requests to /v1/chat/completions with `body.system` as a + * native Anthropic array (not as role="system" messages). The openai→claude + * translator must preserve body.system when no role="system" messages exist. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.ts"; + +describe("#2130: body.system passthrough in openai→claude translator", () => { + it("preserves body.system array when no role=system messages exist", () => { + const body = { + model: "claude-opus-4-7", + max_tokens: 2048, + stream: false, + messages: [ + { + role: "user", + content: [{ type: "text", text: "hi" }], + }, + ], + system: [ + { + type: "text", + text: "You are Claude Code, Anthropic's official CLI for Claude.", + }, + ], + }; + + const result = openaiToClaudeRequest("claude-opus-4-7", body, false); + + assert.ok(result.system, "result.system must be defined"); + assert.ok(Array.isArray(result.system), "result.system must be an array"); + assert.ok( + result.system.some((b) => b.text && b.text.includes("You are Claude Code")), + "result.system must contain the Claude Code system prompt" + ); + }); + + it("preserves body.system string when no role=system messages exist", () => { + const body = { + model: "claude-sonnet-4-6", + max_tokens: 4096, + messages: [{ role: "user", content: "hello" }], + system: "You are a helpful assistant.", + }; + + const result = openaiToClaudeRequest("claude-sonnet-4-6", body, true); + + assert.ok(result.system, "result.system must be defined"); + assert.ok(Array.isArray(result.system), "result.system must be an array"); + assert.equal(result.system[0].text, "You are a helpful assistant."); + }); + + it("merges body.system with role=system messages", () => { + const body = { + model: "claude-opus-4-7", + max_tokens: 2048, + messages: [ + { role: "system", content: "Be concise." }, + { role: "user", content: "hi" }, + ], + system: [ + { + type: "text", + text: "You are Claude Code, Anthropic's official CLI for Claude.", + }, + ], + }; + + const result = openaiToClaudeRequest("claude-opus-4-7", body, false); + + assert.ok(result.system, "result.system must be defined"); + assert.ok(Array.isArray(result.system), "result.system must be an array"); + // Should have the original body.system + the extracted role=system text + assert.ok( + result.system.some((b) => b.text && b.text.includes("You are Claude Code")), + "must contain the body.system content" + ); + assert.ok( + result.system.some((b) => b.text && b.text.includes("Be concise")), + "must contain the role=system message content" + ); + }); + + it("works correctly when neither body.system nor role=system messages exist", () => { + const body = { + model: "claude-sonnet-4-6", + max_tokens: 2048, + messages: [{ role: "user", content: "hello" }], + }; + + const result = openaiToClaudeRequest("claude-sonnet-4-6", body, true); + + assert.equal( + result.system, + undefined, + "result.system should be undefined when no system input exists" + ); + }); +});