From 38e80032976b500f85a8de39a6f0bbaacd1fa653 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 27 Mar 2026 08:33:11 +0530 Subject: [PATCH] fix(anthropic): strip undocumented keys from metadata before sending to API --- litellm/llms/anthropic/chat/transformation.py | 10 +++ .../test_anthropic_completion.py | 78 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 73d1b02c76..9a99f9efc8 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1421,6 +1421,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ): optional_params["metadata"] = {"user_id": _litellm_metadata["user_id"]} + ## Ensure metadata only contains user_id (only documented field in Anthropic Messages API) + if "metadata" in optional_params and isinstance( + optional_params["metadata"], dict + ): + _user_id = optional_params["metadata"].get("user_id") + if _user_id is not None: + optional_params["metadata"] = {"user_id": _user_id} + else: + optional_params.pop("metadata") + # Remove internal LiteLLM parameters that should not be sent to Anthropic API optional_params.pop("is_vertex_request", None) diff --git a/tests/llm_translation/test_anthropic_completion.py b/tests/llm_translation/test_anthropic_completion.py index 8630ba6561..3c1162cd01 100644 --- a/tests/llm_translation/test_anthropic_completion.py +++ b/tests/llm_translation/test_anthropic_completion.py @@ -1800,3 +1800,81 @@ def test_anthropic_structured_output_chat_completion_api(): ) assert response is not None print(f"response: {response}") + + +def _make_transform_request(optional_params: dict, litellm_params: dict) -> dict: + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + return AnthropicConfig().transform_request( + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "hi"}], + optional_params=optional_params, + litellm_params=litellm_params, + headers={}, + ) + + +def test_metadata_only_user_id_passes_through(): + """metadata with only user_id is forwarded as-is.""" + data = _make_transform_request( + optional_params={"metadata": {"user_id": "abc123"}}, + litellm_params={}, + ) + assert data.get("metadata") == {"user_id": "abc123"} + + +def test_metadata_extra_keys_are_stripped(): + """Extra keys in metadata are removed; only user_id is sent.""" + data = _make_transform_request( + optional_params={"metadata": {"user_id": "abc123", "extra_key": "val"}}, + litellm_params={}, + ) + assert data.get("metadata") == {"user_id": "abc123"} + + +def test_metadata_without_user_id_is_dropped(): + """metadata with no user_id is removed entirely.""" + data = _make_transform_request( + optional_params={"metadata": {"only_other_key": "val"}}, + litellm_params={}, + ) + assert "metadata" not in data + + +def test_metadata_user_id_from_litellm_params_strips_extras(): + """user_id from litellm_params metadata is extracted; extra keys are not forwarded.""" + data = _make_transform_request( + optional_params={}, + litellm_params={"metadata": {"user_id": "abc123", "trace_id": "xyz"}}, + ) + assert data.get("metadata") == {"user_id": "abc123"} + + +def test_metadata_filter_applies_to_vertex_anthropic(): + """VertexAIAnthropicConfig inherits the metadata filter.""" + from litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.transformation import ( + VertexAIAnthropicConfig, + ) + + data = VertexAIAnthropicConfig().transform_request( + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "hi"}], + optional_params={"metadata": {"user_id": "u1", "extra": "drop_me"}}, + litellm_params={}, + headers={}, + ) + assert data.get("metadata") == {"user_id": "u1"} + + +def test_metadata_filter_applies_to_azure_anthropic(): + """AzureAnthropicConfig inherits the metadata filter.""" + from litellm.llms.azure_ai.anthropic.transformation import AzureAnthropicConfig + + data = AzureAnthropicConfig().transform_request( + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "hi"}], + optional_params={"metadata": {"user_id": "u2", "extra": "drop_me"}}, + litellm_params={}, + headers={}, + ) + assert data.get("metadata") == {"user_id": "u2"}