fix(proxy): strip leading billing header from system content (#2350)

Claude Code injects a dynamic `x-anthropic-billing-header` line at the
start of `system` content. Its rotating `cch=` token was forwarded into
OpenAI Responses `instructions` and Chat system messages, which broke
upstream prefix prompt cache reuse — a stable ~95k-token prefix was
getting re-charged on every request.

Strip only the leading occurrence in both anthropic_to_openai and
anthropic_to_responses; later occurrences are preserved so user-authored
prompt text containing the same string is not lost.
This commit is contained in:
Jason
2026-04-30 16:54:10 +08:00
parent 518d945eb8
commit 35bce24633
2 changed files with 202 additions and 3 deletions
+122 -2
View File
@@ -6,6 +6,46 @@
use crate::proxy::error::ProxyError;
use serde_json::{json, Value};
const ANTHROPIC_BILLING_HEADER_PREFIX: &str = "x-anthropic-billing-header:";
/// Strip only a leading Claude Code attribution line from system text.
///
/// Claude Code can send dynamic `x-anthropic-billing-header` metadata at the
/// start of `system`. If forwarded into OpenAI Chat messages or Responses
/// `instructions`, the rotating `cch=` value changes the prompt prefix on every
/// request and prevents prefix cache reuse (#2350). Later occurrences are kept
/// to avoid deleting user-authored prompt text.
pub(crate) fn strip_leading_anthropic_billing_header(text: &str) -> &str {
if !text.starts_with(ANTHROPIC_BILLING_HEADER_PREFIX) {
return text;
}
let Some(line_end) = text
.as_bytes()
.iter()
.position(|byte| *byte == b'\n' || *byte == b'\r')
else {
return "";
};
let bytes = text.as_bytes();
let mut rest_start = line_end + 1;
if bytes[line_end] == b'\r' && bytes.get(line_end + 1) == Some(&b'\n') {
rest_start += 1;
}
let rest = &text[rest_start..];
if let Some(stripped) = rest.strip_prefix("\r\n") {
stripped
} else if let Some(stripped) = rest.strip_prefix('\n') {
stripped
} else if let Some(stripped) = rest.strip_prefix('\r') {
stripped
} else {
rest
}
}
/// Detect OpenAI o-series reasoning models (o1, o3, o4-mini, etc.)
/// These models require `max_completion_tokens` instead of `max_tokens`.
pub fn is_openai_o_series(model: &str) -> bool {
@@ -97,12 +137,18 @@ pub fn anthropic_to_openai_with_reasoning_content(
// 处理 system prompt
if let Some(system) = body.get("system") {
if let Some(text) = system.as_str() {
// 单个字符串
messages.push(json!({"role": "system", "content": text}));
let text = strip_leading_anthropic_billing_header(text);
if !text.is_empty() {
messages.push(json!({"role": "system", "content": text}));
}
} else if let Some(arr) = system.as_array() {
// 多个 system message — preserve cache_control for compatible proxies
for msg in arr {
if let Some(text) = msg.get("text").and_then(|t| t.as_str()) {
let text = strip_leading_anthropic_billing_header(text);
if text.is_empty() {
continue;
}
let mut sys_msg = json!({"role": "system", "content": text});
if let Some(cc) = msg.get("cache_control") {
sys_msg["cache_control"] = cc.clone();
@@ -638,6 +684,80 @@ mod tests {
assert_eq!(result["messages"][1]["role"], "user");
}
#[test]
fn test_anthropic_to_openai_strips_leading_billing_header_from_system_string() {
let input = json!({
"model": "claude-3-sonnet",
"max_tokens": 1024,
"system": "x-anthropic-billing-header: cc_version=2.1.119.47e; cc_entrypoint=sdk-cli; cch=a7754;\n\nYou are a helpful assistant.",
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_openai(input).unwrap();
assert_eq!(result["messages"][0]["role"], "system");
assert_eq!(
result["messages"][0]["content"],
"You are a helpful assistant."
);
assert_eq!(result["messages"][1]["role"], "user");
}
#[test]
fn test_anthropic_to_openai_strips_billing_header_from_system_array_parts() {
let input = json!({
"model": "claude-3-sonnet",
"max_tokens": 1024,
"system": [
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.119.47e; cc_entrypoint=sdk-cli; cch=a7754;\n"},
{"type": "text", "text": "Stable prompt"}
],
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_openai(input).unwrap();
assert_eq!(result["messages"][0]["role"], "system");
assert_eq!(result["messages"][0]["content"], "Stable prompt");
assert_eq!(result["messages"][1]["role"], "user");
}
#[test]
fn test_anthropic_to_openai_preserves_prompt_after_billing_header_in_same_part() {
let input = json!({
"model": "claude-3-sonnet",
"max_tokens": 1024,
"system": [
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.119.47e; cc_entrypoint=sdk-cli; cch=a7754;\n\nStable prompt part 1"},
{"type": "text", "text": "Stable prompt part 2"}
],
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_openai(input).unwrap();
assert_eq!(result["messages"][0]["role"], "system");
assert_eq!(
result["messages"][0]["content"],
"Stable prompt part 1\nStable prompt part 2"
);
assert_eq!(result["messages"][1]["role"], "user");
}
#[test]
fn test_anthropic_to_openai_keeps_non_leading_billing_header_text() {
let input = json!({
"model": "claude-3-sonnet",
"max_tokens": 1024,
"system": "Keep this literal:\nx-anthropic-billing-header: example",
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_openai(input).unwrap();
assert_eq!(result["messages"][0]["role"], "system");
assert_eq!(
result["messages"][0]["content"],
"Keep this literal:\nx-anthropic-billing-header: example"
);
}
#[test]
fn test_anthropic_to_openai_with_tools() {
let input = json!({
@@ -35,10 +35,12 @@ pub fn anthropic_to_responses(
// system → instructions (Responses API 使用 instructions 字段)
if let Some(system) = body.get("system") {
let instructions = if let Some(text) = system.as_str() {
text.to_string()
super::transform::strip_leading_anthropic_billing_header(text).to_string()
} else if let Some(arr) = system.as_array() {
arr.iter()
.filter_map(|msg| msg.get("text").and_then(|t| t.as_str()))
.map(super::transform::strip_leading_anthropic_billing_header)
.filter(|text| !text.is_empty())
.collect::<Vec<_>>()
.join("\n\n")
} else {
@@ -611,6 +613,48 @@ mod tests {
assert_eq!(result["input"].as_array().unwrap().len(), 1);
}
#[test]
fn test_anthropic_to_responses_strips_leading_billing_header_from_system_string() {
let input = json!({
"model": "gpt-4o",
"max_tokens": 1024,
"system": "x-anthropic-billing-header: cc_version=2.1.119.47e; cc_entrypoint=sdk-cli; cch=a7754;\n\nYou are a helpful assistant.",
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false, false).unwrap();
assert_eq!(result["instructions"], "You are a helpful assistant.");
}
#[test]
fn test_anthropic_to_responses_strips_billing_header_with_crlf() {
let input = json!({
"model": "gpt-4o",
"max_tokens": 1024,
"system": "x-anthropic-billing-header: cc_version=2.1.119.47e; cc_entrypoint=sdk-cli; cch=a7754;\r\n\r\nYou are a helpful assistant.",
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false, false).unwrap();
assert_eq!(result["instructions"], "You are a helpful assistant.");
}
#[test]
fn test_anthropic_to_responses_keeps_non_leading_billing_header_text() {
let input = json!({
"model": "gpt-4o",
"max_tokens": 1024,
"system": "Keep this literal:\nx-anthropic-billing-header: example",
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false, false).unwrap();
assert_eq!(
result["instructions"],
"Keep this literal:\nx-anthropic-billing-header: example"
);
}
#[test]
fn test_anthropic_to_responses_with_system_array() {
let input = json!({
@@ -627,6 +671,41 @@ mod tests {
assert_eq!(result["instructions"], "Part 1\n\nPart 2");
}
#[test]
fn test_anthropic_to_responses_strips_billing_header_from_system_array_parts() {
let input = json!({
"model": "gpt-4o",
"max_tokens": 1024,
"system": [
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.119.47e; cc_entrypoint=sdk-cli; cch=a7754;\n"},
{"type": "text", "text": "Stable prompt"}
],
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false, false).unwrap();
assert_eq!(result["instructions"], "Stable prompt");
}
#[test]
fn test_anthropic_to_responses_preserves_prompt_after_billing_header_in_same_part() {
let input = json!({
"model": "gpt-4o",
"max_tokens": 1024,
"system": [
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.119.47e; cc_entrypoint=sdk-cli; cch=a7754;\n\nStable prompt part 1"},
{"type": "text", "text": "Stable prompt part 2"}
],
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false, false).unwrap();
assert_eq!(
result["instructions"],
"Stable prompt part 1\n\nStable prompt part 2"
);
}
#[test]
fn test_anthropic_to_responses_with_tools() {
let input = json!({