[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