[Bugfix][Frontend] Strip `x-anthropic-billing-header` billing header from `/v1/chatcompletions` (#59419)
Signed-off-by: Maroon Ayoub <mayoub@redhat.com> Signed-off-by: Robert Shaw <robertgshaw2@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Robert Shaw <robertgshaw2@gmail.com> Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
Maroon Ayoub committed
Oct 1, 2026 at 16:44 UTC
5d9214d6b2ebecb05a9b4cafc2763404acb79289
2 files changed
+114
tests/entrypoints/unit_tests/test_chat_billing_header.py
new
+77
@@ -0,0 +1,77 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Claude Code's attribution header in chat completions system messages."""
4
+
5
+from unittest.mock import MagicMock
6
+
7
+import pytest
8
+
9
+from vllm.entrypoints.chat_utils import _parse_chat_message_content
10
+
11
+HEADER = (
12
+ "x-anthropic-billing-header: cc_version=2.1.220.300; "
13
+ "cc_entrypoint=claude-desktop; cch=7adab;"
14
+)
15
+
16
+
17
+def _parse(message, content_format="openai"):
18
+ tracker = MagicMock()
19
+ tracker.create_parser.return_value.mm_placeholder_storage.return_value = {}
20
+ return _parse_chat_message_content(
21
+ message, tracker, content_format, interleave_strings=False
22
+ )
23
+
24
+
25
+@pytest.mark.skip_global_cleanup
26
+@pytest.mark.parametrize(
27
+ "header",
28
+ [
29
+ HEADER,
30
+ "x-anthropic-billing-header: cc_version=2.1.286.99b; cc_entrypoint=sdk-cli;",
31
+ ],
32
+)
33
+def test_system_header_part_is_dropped(header):
34
+ (msg,) = _parse(
35
+ {
36
+ "role": "system",
37
+ "content": [
38
+ {"type": "text", "text": header},
39
+ {"type": "text", "text": "You are a Claude agent."},
40
+ ],
41
+ }
42
+ )
43
+ assert msg["content"] == [{"type": "text", "text": "You are a Claude agent."}]
44
+
45
+
46
+@pytest.mark.skip_global_cleanup
47
+def test_system_header_line_is_stripped_from_string():
48
+ (msg,) = _parse(
49
+ {"role": "system", "content": HEADER + "\nYou are a Claude agent."},
50
+ content_format="string",
51
+ )
52
+ assert msg["content"] == "You are a Claude agent."
53
+
54
+
55
+@pytest.mark.skip_global_cleanup
56
+def test_requests_differing_only_in_cch_parse_identically():
57
+ def system(cch):
58
+ return {
59
+ "role": "system",
60
+ "content": HEADER.replace("7adab", cch) + "\nYou are a Claude agent.",
61
+ }
62
+
63
+ assert _parse(system("7adab")) == _parse(system("986dd"))
64
+
65
+
66
+@pytest.mark.skip_global_cleanup
67
+@pytest.mark.parametrize(
68
+ "message",
69
+ [
70
+ {"role": "system", "content": "You are a Claude agent."},
71
+ {"role": "system", "content": "Quote: " + HEADER},
72
+ {"role": "user", "content": HEADER + "Hello"},
73
+ ],
74
+)
75
+def test_other_content_is_unchanged(message):
76
+ (msg,) = _parse(message, content_format="string")
77
+ assert msg["content"] == message["content"]
vllm/entrypoints/chat_utils.py
+37
@@ -2035,6 +2035,41 @@ _AssistantParser = partial(cast, ChatCompletionAssistantMessageParam)
2035
_ToolParser = partial(cast, ChatCompletionToolMessageParam)
2036
2037
2038
+_CLAUDE_CODE_BILLING_HEADER = "x-anthropic-billing-header"
2039
+
2040
+
2041
+def _strip_claude_code_billing_header(
2042
+ parts: Iterable[ChatCompletionContentPartParam],
2043
+) -> list[ChatCompletionContentPartParam]:
2044
+ """Drop Claude Code's attribution header line from system text parts.
2045
+
2046
+ Its ``cch`` value changes on every request, so a prompt that keeps it
2047
+ misses the prefix cache from the header on. Gateways that translate
2048
+ Anthropic requests to chat completions keep it; ``/v1/messages`` drops
2049
+ the same block.
2050
+ """
2051
+ out: list[ChatCompletionContentPartParam] = []
2052
+ for part in parts:
2053
+ if isinstance(part, str):
2054
+ text: object = part
2055
+ elif isinstance(part, dict) and part.get("type") == "text":
2056
+ text = part.get("text")
2057
+ else:
2058
+ text = None
2059
+ if not (isinstance(text, str) and text.startswith(_CLAUDE_CODE_BILLING_HEADER)):
2060
+ out.append(part)
2061
+ continue
2062
+ rest = text.partition("\n")[2]
2063
+ if not rest:
2064
+ continue
2065
+ out.append(
2066
+ cast(ChatCompletionContentPartParam, rest)
2067
+ if isinstance(part, str)
2068
+ else cast(ChatCompletionContentPartParam, {**part, "text": rest})
2069
+ )
2070
+ return out
2071
+
2072
+
2073
def _parse_chat_message_content(
2074
message: ChatCompletionMessageParam,
2075
mm_tracker: BaseMultiModalItemTracker,
@@ -2050,6 +2085,8 @@ def _parse_chat_message_content(
2085
content = []
2086
elif isinstance(content, str):
2087
content = [ChatCompletionContentPartTextParam(type="text", text=content)]
2088
+ if role == "system":
2089
+ content = _strip_claude_code_billing_header(content) # type: ignore[arg-type]
2090
result = _parse_chat_message_content_parts(
2091
role,
2092
content, # type: ignore