fix(native): preserve opaque metadata eligibility

This commit is contained in:
Yujong Lee 2026-09-05 18:08:35 -07:00 committed by yujonglee
parent 3d359ba954
commit 242ab68201
7 changed files with 47 additions and 3 deletions

View file

@ -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()

View file

@ -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()
},

View file

@ -49,6 +49,7 @@ impl BedrockOptions {
#[derive(Clone, Debug, Default)]
pub struct AnthropicOptions {
pub user_id: Option<String>,
pub has_user_id: bool,
}
#[derive(Clone, Debug, Default)]

View file

@ -44,12 +44,14 @@ impl From<NativeBedrockOptions> for litellm_core::request_options::BedrockOption
#[derive(FromPyObject)]
struct NativeAnthropicOptions {
user_id: Option<String>,
has_user_id: bool,
}
impl From<NativeAnthropicOptions> for litellm_core::request_options::AnthropicOptions {
fn from(input: NativeAnthropicOptions) -> Self {
Self {
user_id: input.user_id,
has_user_id: input.has_user_id,
}
}
}

View file

@ -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

View file

@ -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")

View file

@ -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])