mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(translator): preserve body.system in openai→claude when Claude Code sends native format (#2130)
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.
This commit is contained in:
79
CHANGELOG.md
79
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
103
tests/unit/fix-2130-system-passthrough.test.ts
Normal file
103
tests/unit/fix-2130-system-passthrough.test.ts
Normal file
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user