Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions open-sse/translator/request/claude-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
type JsonRecord = Record<string, unknown>;
const TOOL_CHOICE_ANY = ["a", "n", "y"].join("");

/**
* Port of decolua/9router commit 0aaa5ab3 (closes prompt-cache instability):
* Anthropic injects a dynamic `x-anthropic-billing-header: <value>` line at
* the top of some system prompts. When we translate Claude → OpenAI and
* forward to a non-Anthropic upstream, that line both leaks into the
* assistant prompt and rotates per request, destroying prompt-cache hits.
* Strip it from each system entry before assembling the OpenAI request.
*/
function stripAnthropicBillingHeader(text: unknown): string {
if (typeof text !== "string") return "";
return text.replace(/^x-anthropic-billing-header:[^\n]*(?:\r?\n)?/i, "");
}

/**
* Normalize tool input schema for OpenAI compatibility.
* OpenAI strict mode requires `properties: {}` on object-type schemas,
Expand Down Expand Up @@ -108,8 +121,11 @@ export function claudeToOpenAIRequest(model, body, stream, credentials: unknown
// System message
if (body.system) {
const systemContent = Array.isArray(body.system)
? body.system.map((s) => s.text || "").join("\n")
: body.system;
? body.system
.map((s) => stripAnthropicBillingHeader(s.text || ""))
.filter(Boolean)
.join("\n")
: stripAnthropicBillingHeader(body.system);

if (systemContent) {
result.messages.push({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Port of decolua/9router commit 0aaa5ab3:
* The Anthropic upstream injects a dynamic `x-anthropic-billing-header: ...`
* line at the top of the Claude system prompt for some routes. When we
* translate Claude → OpenAI and forward to an OpenAI-compatible upstream,
* that line ends up as part of the assistant's system message — which
* (a) confuses non-Anthropic models and (b) keeps changing across requests
* and so destroys prompt-cache hits.
*
* Strip a leading `x-anthropic-billing-header: <value>` line from each
* system entry before assembling the OpenAI request.
*/

import test from "node:test";
import assert from "node:assert/strict";

const { claudeToOpenAIRequest } = await import(
"../../open-sse/translator/request/claude-to-openai.ts"
);

test("Claude -> OpenAI strips x-anthropic-billing-header from array system entries", () => {
const result = claudeToOpenAIRequest(
"gpt-4o",
{
system: [
{ text: "x-anthropic-billing-header: abc123\nReal system rule A" },
{ text: "Real system rule B" },
],
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
},
true
) as { messages: Array<{ role: string; content: string }> };

const sys = result.messages.find((m) => m.role === "system");
assert.ok(sys, "system message present");
assert.equal(
sys!.content.includes("x-anthropic-billing-header"),
false,
"billing header line must be stripped"
);
assert.equal(
sys!.content.includes("Real system rule A"),
true,
"real rule must survive"
);
assert.equal(
sys!.content.includes("Real system rule B"),
true,
"second block must survive"
);
});

test("Claude -> OpenAI strips x-anthropic-billing-header from a string system field", () => {
const result = claudeToOpenAIRequest(
"gpt-4o",
{
system: "x-anthropic-billing-header: zzz\nFollow these rules.",
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
},
true
) as { messages: Array<{ role: string; content: string }> };

const sys = result.messages.find((m) => m.role === "system");
assert.ok(sys, "system message present");
assert.equal(sys!.content, "Follow these rules.");
});

test("Claude -> OpenAI leaves unrelated system content intact (no false positives)", () => {
const result = claudeToOpenAIRequest(
"gpt-4o",
{
system: "Normal system prompt with no billing header.",
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
},
true
) as { messages: Array<{ role: string; content: string }> };

const sys = result.messages.find((m) => m.role === "system");
assert.equal(sys!.content, "Normal system prompt with no billing header.");
});
Loading