mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
Preserve x-anthropic-billing-header system blocks for first-party Anthropic (#29584)
* Preserve x-anthropic-billing-header system blocks for first-party Anthropic PR #20951 strips system blocks beginning with "x-anthropic-billing-header:" for every Anthropic target. That block is how the first-party Anthropic API recognizes Claude Code subscription (OAuth) traffic, so dropping it makes requests that carry only that block, such as the auto-mode tool-safety classifier, fail with a misleading 429 rate_limit_error; normal turns still work because they also carry the "You are Claude Code" identity block. Gate the strip behind should_strip_billing_metadata(), defaulting to False on the first-party AnthropicConfig and AnthropicMessagesConfig so the block is kept, and overridden to True on the providers that reach these transforms and reject the block (Bedrock platform, Vertex, Azure for the chat path; Minimax, Azure, DeepSeek for the messages path). Behavior for those providers is unchanged. * Strip billing header on Bedrock invoke and Vertex messages pass-through Two more subclasses reach the gated strip but inherited keep-by-default. AmazonAnthropicClaudeConfig (Bedrock invoke) calls AnthropicConfig.transform_request, which calls translate_system_message, and VertexAIPartnerModelsAnthropicMessagesConfig (Vertex messages pass-through) calls super().transform_anthropic_messages_request. Override should_strip_billing_metadata() to True on both. Add a parametrized test asserting the flag for every first-party base (False) and provider subclass (True), covering all overrides, plus a translate_system_message regression test for the Bedrock invoke path.
This commit is contained in:
parent
49600c67b4
commit
164734cef8
11 changed files with 218 additions and 10 deletions
|
|
@ -1607,6 +1607,15 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
)
|
||||
return _tool
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
"""
|
||||
Whether to drop x-anthropic-billing-header system blocks before sending upstream.
|
||||
|
||||
The first-party Anthropic API uses these blocks for Claude Code attribution, so the
|
||||
base config keeps them. Providers that reject them (e.g. Bedrock) override this to True.
|
||||
"""
|
||||
return False
|
||||
|
||||
def translate_system_message(
|
||||
self, messages: List[AllMessageValues]
|
||||
) -> List[AnthropicSystemMessageContent]:
|
||||
|
|
@ -1614,7 +1623,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
Translate system message to anthropic format.
|
||||
|
||||
Removes system message from the original list and returns a new list of anthropic system message content.
|
||||
Filters out system messages containing x-anthropic-billing-header metadata.
|
||||
When should_strip_billing_metadata() is True, x-anthropic-billing-header system blocks are dropped.
|
||||
"""
|
||||
system_prompt_indices = []
|
||||
anthropic_system_message_list: List[AnthropicSystemMessageContent] = []
|
||||
|
|
@ -1626,10 +1635,9 @@ 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:"
|
||||
):
|
||||
if self.should_strip_billing_metadata() and system_message_block[
|
||||
"content"
|
||||
].startswith("x-anthropic-billing-header:"):
|
||||
continue
|
||||
anthropic_system_message_content = AnthropicSystemMessageContent(
|
||||
type="text",
|
||||
|
|
@ -1648,9 +1656,9 @@ 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
|
||||
if (
|
||||
_content.get("type") == "text"
|
||||
self.should_strip_billing_metadata()
|
||||
and _content.get("type") == "text"
|
||||
and text_value
|
||||
and text_value.startswith("x-anthropic-billing-header:")
|
||||
):
|
||||
|
|
|
|||
|
|
@ -84,6 +84,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
if isinstance(content, list):
|
||||
_process_content_list(content)
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
"""
|
||||
Whether to drop x-anthropic-billing-header system blocks before sending upstream.
|
||||
|
||||
The first-party Anthropic API uses these blocks for Claude Code attribution, so the
|
||||
base config keeps them. Providers that reject them override this to True.
|
||||
"""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _filter_billing_headers_from_system(system_param):
|
||||
"""
|
||||
|
|
@ -286,14 +295,12 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
optional_params=anthropic_messages_optional_request_params,
|
||||
)
|
||||
|
||||
# Filter out x-anthropic-billing-header from system messages
|
||||
system_param = anthropic_messages_optional_request_params.get("system")
|
||||
if system_param is not None:
|
||||
if self.should_strip_billing_metadata() and system_param is not None:
|
||||
filtered_system = self._filter_billing_headers_from_system(system_param)
|
||||
if filtered_system is not None and len(filtered_system) > 0:
|
||||
anthropic_messages_optional_request_params["system"] = filtered_system
|
||||
else:
|
||||
# Remove system parameter if all content was filtered out
|
||||
anthropic_messages_optional_request_params.pop("system", None)
|
||||
|
||||
# Transform context_management from OpenAI format to Anthropic format if needed
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
and Azure endpoint format.
|
||||
"""
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def validate_anthropic_messages_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ class AzureAnthropicConfig(AnthropicConfig):
|
|||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "azure_ai"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "bedrock"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> List[str]:
|
||||
return AnthropicConfig.get_supported_openai_params(self, model)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ class BedrockClaudePlatformConfig(BedrockClaudePlatformMixin, AnthropicConfig):
|
|||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "bedrock"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ class DeepSeekAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "deepseek"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_api_key(api_key: Optional[str] = None) -> Optional[str]:
|
||||
return api_key or get_secret_str("DEEPSEEK_API_KEY") or litellm.api_key
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ class MinimaxMessagesConfig(AnthropicMessagesConfig):
|
|||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "minimax"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_api_key(api_key: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ from ..output_params_utils import sanitize_vertex_anthropic_output_params
|
|||
|
||||
|
||||
class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, VertexBase):
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def validate_anthropic_messages_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ class VertexAIAnthropicConfig(AnthropicConfig):
|
|||
def custom_llm_provider(self) -> Optional[str]:
|
||||
return "vertex_ai"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def _add_context_management_beta_headers(
|
||||
self, beta_set: set, context_management: dict
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -5092,6 +5092,175 @@ def test_map_tool_helper_collision_prefers_definitions_over_components_schemas()
|
|||
assert transformed["input_schema"]["properties"]["from_components"] == expected
|
||||
|
||||
|
||||
BILLING_HEADER_BLOCK = {
|
||||
"type": "text",
|
||||
"text": "x-anthropic-billing-header: cc_version=1.0.abc; cc_entrypoint=cli; cch=00000;",
|
||||
}
|
||||
|
||||
|
||||
def _system_with_billing_header(real_text: str) -> list:
|
||||
return [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [BILLING_HEADER_BLOCK, {"type": "text", "text": real_text}],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_translate_system_message_keeps_billing_header_for_first_party_anthropic():
|
||||
config = AnthropicConfig()
|
||||
assert config.should_strip_billing_metadata() is False
|
||||
|
||||
result = config.translate_system_message(
|
||||
messages=_system_with_billing_header(
|
||||
"You are Claude Code, Anthropic's official CLI for Claude."
|
||||
)
|
||||
)
|
||||
|
||||
texts = [block["text"] for block in result]
|
||||
assert any(t.startswith("x-anthropic-billing-header:") for t in texts)
|
||||
assert "You are Claude Code, Anthropic's official CLI for Claude." in texts
|
||||
|
||||
|
||||
def test_translate_system_message_strips_billing_header_for_bedrock():
|
||||
from litellm.llms.bedrock.claude_platform.transformation import (
|
||||
BedrockClaudePlatformConfig,
|
||||
)
|
||||
|
||||
config = BedrockClaudePlatformConfig()
|
||||
assert config.should_strip_billing_metadata() is True
|
||||
|
||||
result = config.translate_system_message(
|
||||
messages=_system_with_billing_header("real system prompt")
|
||||
)
|
||||
|
||||
texts = [block["text"] for block in result]
|
||||
assert all(not t.startswith("x-anthropic-billing-header:") for t in texts)
|
||||
assert "real system prompt" in texts
|
||||
|
||||
|
||||
def test_anthropic_messages_request_keeps_billing_header_for_first_party():
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
config = AnthropicMessagesConfig()
|
||||
assert config.should_strip_billing_metadata() is False
|
||||
|
||||
optional_params = {
|
||||
"max_tokens": 16,
|
||||
"system": [
|
||||
BILLING_HEADER_BLOCK,
|
||||
{"type": "text", "text": "real system prompt"},
|
||||
],
|
||||
}
|
||||
result = config.transform_anthropic_messages_request(
|
||||
model="claude-3-5-sonnet-latest",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
anthropic_messages_optional_request_params=optional_params,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
texts = [block["text"] for block in result["system"]]
|
||||
assert any(t.startswith("x-anthropic-billing-header:") for t in texts)
|
||||
|
||||
|
||||
def test_anthropic_messages_request_strips_billing_header_for_minimax():
|
||||
from litellm.llms.minimax.messages.transformation import MinimaxMessagesConfig
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
config = MinimaxMessagesConfig()
|
||||
assert config.should_strip_billing_metadata() is True
|
||||
|
||||
optional_params = {
|
||||
"max_tokens": 16,
|
||||
"system": [
|
||||
BILLING_HEADER_BLOCK,
|
||||
{"type": "text", "text": "real system prompt"},
|
||||
],
|
||||
}
|
||||
result = config.transform_anthropic_messages_request(
|
||||
model="MiniMax-M2",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
anthropic_messages_optional_request_params=optional_params,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
texts = [block["text"] for block in result.get("system", [])]
|
||||
assert all(not t.startswith("x-anthropic-billing-header:") for t in texts)
|
||||
|
||||
|
||||
def test_translate_system_message_strips_billing_header_for_bedrock_invoke():
|
||||
from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import (
|
||||
AmazonAnthropicClaudeConfig,
|
||||
)
|
||||
|
||||
config = AmazonAnthropicClaudeConfig()
|
||||
assert config.should_strip_billing_metadata() is True
|
||||
|
||||
result = config.translate_system_message(
|
||||
messages=_system_with_billing_header("real system prompt")
|
||||
)
|
||||
|
||||
texts = [block["text"] for block in result]
|
||||
assert all(not t.startswith("x-anthropic-billing-header:") for t in texts)
|
||||
assert "real system prompt" in texts
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"module_path, class_name, expected_strip",
|
||||
[
|
||||
("litellm.llms.anthropic.chat.transformation", "AnthropicConfig", False),
|
||||
(
|
||||
"litellm.llms.anthropic.experimental_pass_through.messages.transformation",
|
||||
"AnthropicMessagesConfig",
|
||||
False,
|
||||
),
|
||||
(
|
||||
"litellm.llms.bedrock.claude_platform.transformation",
|
||||
"BedrockClaudePlatformConfig",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation",
|
||||
"AmazonAnthropicClaudeConfig",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.transformation",
|
||||
"VertexAIAnthropicConfig",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"litellm.llms.azure_ai.anthropic.transformation",
|
||||
"AzureAnthropicConfig",
|
||||
True,
|
||||
),
|
||||
("litellm.llms.minimax.messages.transformation", "MinimaxMessagesConfig", True),
|
||||
(
|
||||
"litellm.llms.azure_ai.anthropic.messages_transformation",
|
||||
"AzureAnthropicMessagesConfig",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"litellm.llms.deepseek.messages.transformation",
|
||||
"DeepSeekAnthropicMessagesConfig",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.experimental_pass_through.transformation",
|
||||
"VertexAIPartnerModelsAnthropicMessagesConfig",
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_should_strip_billing_metadata_by_provider(
|
||||
module_path, class_name, expected_strip
|
||||
):
|
||||
import importlib
|
||||
|
||||
config_cls = getattr(importlib.import_module(module_path), class_name)
|
||||
assert config_cls().should_strip_billing_metadata() is expected_strip
|
||||
def test_namespace_tool_flat_nested_tools_are_extracted():
|
||||
"""Codex sends nested tools in flat format {type, name, description, parameters} with no 'function' wrapper.
|
||||
These must be normalized and mapped without raising KeyError: 'function'."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue