diff --git a/litellm-rust/crates/core/src/chat_completions/mod.rs b/litellm-rust/crates/core/src/chat_completions/mod.rs index 8d51f2826b6..f415d825a10 100644 --- a/litellm-rust/crates/core/src/chat_completions/mod.rs +++ b/litellm-rust/crates/core/src/chat_completions/mod.rs @@ -84,8 +84,8 @@ fn unsupported_reason( "anthropic" => options .anthropic .as_ref() - .and_then(|anthropic| anthropic.user_id.as_ref()) - .map(|_| Unsupported("LiteLLM user metadata")), + .is_some_and(|anthropic| anthropic.has_user_id) + .then_some(Unsupported("LiteLLM user metadata")), "bedrock" => options .bedrock .as_ref() diff --git a/litellm-rust/crates/core/src/chat_completions/tests.rs b/litellm-rust/crates/core/src/chat_completions/tests.rs index 00a5457743e..861db7a2db1 100644 --- a/litellm-rust/crates/core/src/chat_completions/tests.rs +++ b/litellm-rust/crates/core/src/chat_completions/tests.rs @@ -895,6 +895,7 @@ fn preflight_and_execution_share_provider_metadata_eligibility() { custom_llm_provider: Some("anthropic".into()), anthropic: Some(AnthropicOptions { user_id: Some("u-123".into()), + has_user_id: true, }), ..Default::default() }, diff --git a/litellm-rust/crates/core/src/request_options.rs b/litellm-rust/crates/core/src/request_options.rs index 66c7ed89366..4377d2d4066 100644 --- a/litellm-rust/crates/core/src/request_options.rs +++ b/litellm-rust/crates/core/src/request_options.rs @@ -49,6 +49,7 @@ impl BedrockOptions { #[derive(Clone, Debug, Default)] pub struct AnthropicOptions { pub user_id: Option, + pub has_user_id: bool, } #[derive(Clone, Debug, Default)] diff --git a/litellm-rust/crates/python-bridge/src/marshal.rs b/litellm-rust/crates/python-bridge/src/marshal.rs index ca2f70696d4..abc1c020772 100644 --- a/litellm-rust/crates/python-bridge/src/marshal.rs +++ b/litellm-rust/crates/python-bridge/src/marshal.rs @@ -44,12 +44,14 @@ impl From for litellm_core::request_options::BedrockOption #[derive(FromPyObject)] struct NativeAnthropicOptions { user_id: Option, + has_user_id: bool, } impl From for litellm_core::request_options::AnthropicOptions { fn from(input: NativeAnthropicOptions) -> Self { Self { user_id: input.user_id, + has_user_id: input.has_user_id, } } } diff --git a/litellm/rust_bridge/request.py b/litellm/rust_bridge/request.py index 31fc1e0e47d..cb23583cf17 100644 --- a/litellm/rust_bridge/request.py +++ b/litellm/rust_bridge/request.py @@ -26,6 +26,7 @@ class NativeBedrockOptions: @dataclass(frozen=True, slots=True) class NativeAnthropicOptions: user_id: str | None = None + has_user_id: bool = False @dataclass(frozen=True, slots=True) @@ -57,7 +58,10 @@ def bedrock_options(params: Mapping[str, object]) -> NativeBedrockOptions: def anthropic_options(litellm_params: Mapping[str, object] | None) -> NativeAnthropicOptions: metadata = None if litellm_params is None else litellm_params.get("metadata") user_id = metadata.get("user_id") if isinstance(metadata, Mapping) else None - return NativeAnthropicOptions(user_id=user_id if isinstance(user_id, str) else None) + return NativeAnthropicOptions( + user_id=user_id if isinstance(user_id, str) else None, + has_user_id=user_id is not None, + ) def vertex_options(params: Mapping[str, object]) -> NativeVertexOptions: @@ -68,6 +72,7 @@ def vertex_options(params: Mapping[str, object]) -> NativeVertexOptions: location=location if isinstance(location, str) else None, ) + from typing_extensions import ReadOnly, TypedDict diff --git a/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py b/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py index dc5d29d78bc..45ef8aa59aa 100644 --- a/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py +++ b/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py @@ -254,6 +254,37 @@ async def test_public_messages_routes_provider_acceptance(monkeypatch, asynchron assert calls[0]["body"]["max_tokens"] == 64 +def test_public_messages_strips_provider_specific_fields_before_native_dispatch(): + native = RecordingMessages() + litellm.rust(True) + rust_messages.set_rust_messages(messages=native) + messages = [ + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "tool_1", + "name": "lookup", + "input": {}, + "provider_specific_fields": {"thought_signature": "signature"}, + } + ], + } + ] + + litellm.anthropic.messages.create( + model="anthropic/test-model", + max_tokens=64, + messages=messages, + api_key="key", + ) + + content = native.calls[0]["body"]["messages"][0]["content"][0] + assert "provider_specific_fields" not in content + assert "provider_specific_fields" in messages[0]["content"][0] + + @pytest.mark.parametrize("condition", ["disabled", "declined", "missing_binding", "missing_preflight", "stream"]) def test_public_messages_fallback_once(monkeypatch, condition): module = importlib.import_module("litellm.llms.anthropic.experimental_pass_through.messages.handler") diff --git a/tests/test_litellm/rust_bridge/test_chat_completions.py b/tests/test_litellm/rust_bridge/test_chat_completions.py index 5131687cdc6..85e3e050a46 100644 --- a/tests/test_litellm/rust_bridge/test_chat_completions.py +++ b/tests/test_litellm/rust_bridge/test_chat_completions.py @@ -477,6 +477,10 @@ def test_typed_capability_and_provider_metadata_facts_are_isolated(): assert context.capabilities.request_format == "native" assert context.capabilities.has_agentic_hook is True assert anthropic.user_id == "user-123" + assert anthropic.has_user_id is True + assert anthropic_options({"metadata": {"user_id": object()}}).has_user_id is True + assert anthropic_options({"metadata": {"user_id": None}}).has_user_id is False + @pytest.mark.parametrize("provider", ["anthropic", "bedrock", "openai"]) @pytest.mark.parametrize("asynchronous", [False, True])