fix(anthropic): only strip x-anthropic-billing-header on Bedrock targets

`AnthropicConfig.translate_system_message` previously stripped any
system block starting with `x-anthropic-billing-header:` for every
Anthropic-derived config. This was added in #20951 because Bedrock
rejects the header as a reserved keyword.

Anthropic-direct (the OAuth path used by Claude Code) needs to keep
that block: it's the recognition signal that lets Max/Pro subscription
tokens (`sk-ant-oat01-*`) authenticate. Stripping it caused the
tool-safety classifier and other Claude Code modes to fail with a
misleading `rate_limit_error` 429 (#29572).

Gate the strip on a new class attribute `_strips_x_anthropic_billing_header`,
defaulting to `False` on `AnthropicConfig` (Anthropic-direct), set to
`True` on the two Bedrock subclasses (`AmazonAnthropicClaudeConfig`,
`BedrockClaudePlatformConfig`) so Bedrock behavior is unchanged.

Regression tests cover:
- Anthropic-direct keeps the header (string content + list content)
- Bedrock invoke strips it (existing behavior preserved)
- Bedrock claude_platform strips it (existing behavior preserved)

Closes #29572
This commit is contained in:
HumphreySun98 2026-06-04 15:02:42 -05:00
parent f9142d7961
commit 0d4cf7668b
4 changed files with 116 additions and 11 deletions

View file

@ -246,6 +246,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
metadata: Optional[dict] = None
system: Optional[str] = None
# Bedrock rejects `x-anthropic-billing-header:` text blocks in the system
# array as reserved-keyword (see #20951), so its subclasses set this to True
# to drop them on the wire. Anthropic-direct (this base class) must leave
# them in place: Claude Code uses them as the recognition signal for
# Max/Pro OAuth tokens, and stripping them causes 429s on Anthropic (#29572).
_strips_x_anthropic_billing_header: bool = False
def __init__(
self,
max_tokens: Optional[int] = None,
@ -1639,10 +1646,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
# Skip empty text blocks - Anthropic API raises errors for empty text
if not system_message_block["content"]:
continue
# Skip system messages containing x-anthropic-billing-header metadata
if system_message_block["content"].startswith(
"x-anthropic-billing-header:"
):
# Skip system messages containing x-anthropic-billing-header
# metadata only for targets that reject it (e.g. Bedrock).
# Anthropic-direct keeps the header so Claude Code's
# Max/Pro OAuth recognition signal isn't dropped (#29572).
if self._strips_x_anthropic_billing_header and system_message_block[
"content"
].startswith("x-anthropic-billing-header:"):
continue
anthropic_system_message_content = AnthropicSystemMessageContent(
type="text",
@ -1661,9 +1671,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
text_value = _content.get("text")
if _content.get("type") == "text" and not text_value:
continue
# Skip system messages containing x-anthropic-billing-header metadata
# Skip x-anthropic-billing-header text blocks only for
# targets that reject the header (#29572).
if (
_content.get("type") == "text"
self._strips_x_anthropic_billing_header
and _content.get("type") == "text"
and text_value
and text_value.startswith("x-anthropic-billing-header:")
):
@ -2449,11 +2461,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
tool_calls,
)
json_mode_message, tool_calls_for_message, json_extra_content = (
self._resolve_json_mode_non_streaming(
json_mode=json_mode,
tool_calls=tool_calls,
)
(
json_mode_message,
tool_calls_for_message,
json_extra_content,
) = self._resolve_json_mode_non_streaming(
json_mode=json_mode,
tool_calls=tool_calls,
)
merged_text = text_content or ""
if json_extra_content:

View file

@ -56,6 +56,10 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
anthropic_version: str = "bedrock-2023-05-31"
# Bedrock rejects `x-anthropic-billing-header:` text blocks in the system
# array as a reserved keyword (#20951), so strip them on the wire.
_strips_x_anthropic_billing_header: bool = True
@property
def custom_llm_provider(self) -> Optional[str]:
return "bedrock"

View file

@ -13,6 +13,10 @@ class BedrockClaudePlatformConfig(BedrockClaudePlatformMixin, AnthropicConfig):
Bedrock Claude Platform uses Anthropic's Messages API with AWS gateway auth.
"""
# Bedrock rejects `x-anthropic-billing-header:` text blocks in the system
# array as a reserved keyword (#20951), so strip them on the wire.
_strips_x_anthropic_billing_header: bool = True
@property
def custom_llm_provider(self) -> Optional[str]:
return "bedrock"

View file

@ -5090,3 +5090,86 @@ def test_map_tool_helper_collision_prefers_definitions_over_components_schemas()
# Cross-namespace ref *also* resolves to the `definitions` body because
# ``unpack_defs`` keys by last path segment -- documented limitation.
assert transformed["input_schema"]["properties"]["from_components"] == expected
def test_translate_system_message_preserves_x_anthropic_billing_header_on_anthropic_direct():
"""Regression for #29572.
Anthropic-direct must preserve `x-anthropic-billing-header:` text blocks
in the system array: Claude Code uses them as the recognition signal for
Max/Pro OAuth tokens, and stripping them causes 429s on Anthropic.
Only Bedrock subclasses strip the header.
"""
config = AnthropicConfig()
# String content path
messages_str = [
{"role": "system", "content": "x-anthropic-billing-header: claude-code"},
{"role": "user", "content": "hi"},
]
result = config.translate_system_message(messages_str)
assert len(result) == 1
assert result[0]["text"] == "x-anthropic-billing-header: claude-code"
# List content path
messages_list = [
{
"role": "system",
"content": [
{"type": "text", "text": "x-anthropic-billing-header: claude-code"},
{
"type": "text",
"text": "You are a security monitor for autonomous AI coding agents.",
},
],
},
{"role": "user", "content": "hi"},
]
result = config.translate_system_message(messages_list)
assert [block["text"] for block in result] == [
"x-anthropic-billing-header: claude-code",
"You are a security monitor for autonomous AI coding agents.",
]
def test_translate_system_message_strips_x_anthropic_billing_header_on_bedrock_invoke():
"""Bedrock (AmazonAnthropicClaudeConfig) must strip `x-anthropic-billing-header:`
blocks because Bedrock rejects them as a reserved keyword (#20951).
Verifies the per-subclass `_strips_x_anthropic_billing_header` flag wired
up for #29572 doesn't regress that behavior.
"""
from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import (
AmazonAnthropicClaudeConfig,
)
config = AmazonAnthropicClaudeConfig()
messages = [
{
"role": "system",
"content": [
{"type": "text", "text": "x-anthropic-billing-header: claude-code"},
{"type": "text", "text": "You are a helpful assistant."},
],
},
{"role": "user", "content": "hi"},
]
result = config.translate_system_message(messages)
assert [block["text"] for block in result] == ["You are a helpful assistant."]
def test_translate_system_message_strips_x_anthropic_billing_header_on_bedrock_claude_platform():
"""Same as above for BedrockClaudePlatformConfig."""
from litellm.llms.bedrock.claude_platform.transformation import (
BedrockClaudePlatformConfig,
)
config = BedrockClaudePlatformConfig()
messages = [
{"role": "system", "content": "x-anthropic-billing-header: claude-code"},
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "hi"},
]
result = config.translate_system_message(messages)
assert [block["text"] for block in result] == ["You are a helpful assistant."]