Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,16 @@ def transform_request(
):
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")
Comment on lines +1428 to +1432

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing _valid_user_id validation creates inconsistency

The two existing paths that populate metadata.user_id both call _valid_user_id() to reject emails and phone numbers (Anthropic rejects those values — see the comment at line 1032: "anthropic fails on emails"). This new filter skips that check entirely.

If a caller passes metadata={"user_id": "user@example.com"} directly in optional_params, the new code will forward {"user_id": "user@example.com"} to Anthropic and receive an API error, while the same value arriving via the user parameter or litellm_params.metadata would be silently dropped.

Suggested change
_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")
_user_id = optional_params["metadata"].get("user_id")
if _user_id is not None and _valid_user_id(str(_user_id)):
optional_params["metadata"] = {"user_id": _user_id}
else:
optional_params.pop("metadata")

Comment on lines +1424 to +1432

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent key stripping without a log warning

Extra metadata keys are dropped silently. A user who passes metadata={"user_id": "x", "trace_id": "y"} will see trace_id disappear with no indication of why. Other spots in this file already use litellm.verbose_logger.warning() for similar "we modified your input" situations (e.g. line 1366). Adding a warning here would make it much easier to debug.

Suggested change
## 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")
## 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")
_extra_keys = [k for k in optional_params["metadata"] if k != "user_id"]
if _extra_keys:
litellm.verbose_logger.warning(
"Anthropic metadata only supports 'user_id'. "
"Dropping unsupported keys: %s",
_extra_keys,
)
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)

Expand Down
78 changes: 78 additions & 0 deletions tests/llm_translation/test_anthropic_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Loading