mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
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:
parent
f9142d7961
commit
0d4cf7668b
4 changed files with 116 additions and 11 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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."]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue