diff --git a/litellm/integrations/otel/model/metadata.py b/litellm/integrations/otel/model/metadata.py index 8b3a5fc3fd5..5f90e70e119 100644 --- a/litellm/integrations/otel/model/metadata.py +++ b/litellm/integrations/otel/model/metadata.py @@ -49,7 +49,8 @@ if TYPE_CHECKING: from litellm.types.utils import StandardLoggingPayload LANGFUSE_TRACE_NAME_HEADER: Final = "langfuse_trace_name" -REQUESTER_METADATA_PATH: Final = "requester_metadata." +REQUESTER_METADATA_KEY: Final = "requester_metadata" +REQUESTER_METADATA_PATH: Final = f"{REQUESTER_METADATA_KEY}." @dataclass(frozen=True) @@ -106,8 +107,9 @@ class RequestIdentity: guardrail, or service span is created — so the whole request's spans inherit identity, not just the LLM-call span. Metadata sub-keys use the ``user_api_key_*`` names that ``baggage.DEFAULT_BAGGAGE_METADATA_KEYS`` - promotes; ``request_metadata`` (the proxy's per-request metadata dict) is - flattened to dotted keys so ``requester_metadata.`` resolves too. + promotes; ``request_metadata`` (the caller's ``requester_metadata`` + snapshot) is flattened to dotted keys so ``requester_metadata.`` + resolves too. """ get: Final = lambda name: getattr(auth, name, None) # noqa: E731 auth_meta: Final = tuple( @@ -358,21 +360,21 @@ def model_from_request_data(data: object) -> str | None: def metadata_from_request_data(data: object) -> Mapping[str, object] | None: - """The proxy's per-request metadata dict from a pre-call ``data`` dict. + """The caller's ``requester_metadata`` snapshot from a pre-call ``data`` dict, keyed under its wrapper. - The proxy writes it under ``metadata`` or ``litellm_metadata`` depending on - the route; the one carrying the ``requester_metadata`` snapshot wins. + The proxy stores it under ``metadata`` or ``litellm_metadata`` depending on the route; + the proxy-owned siblings (``user_api_key_*``, ``requester_ip_address``) are not read. """ top: Final = _as_str_mapping(data) if top is None: return None - candidates: Final = tuple( - nested for name in ("metadata", "litellm_metadata") if (nested := _as_str_mapping(top.get(name))) is not None - ) - return next( - (c for c in candidates if isinstance(c.get("requester_metadata"), Mapping)), - candidates[0] if candidates else None, + snapshots: Final = tuple( + snapshot + for name in ("metadata", "litellm_metadata") + if (nested := _as_str_mapping(top.get(name))) is not None + and (snapshot := _as_str_mapping(nested.get(REQUESTER_METADATA_KEY))) is not None ) + return MappingProxyType({REQUESTER_METADATA_KEY: snapshots[0]}) if snapshots else None def flatten_metadata(raw: Mapping[str, object]) -> Iterator[tuple[str, str]]: diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_logger.py b/tests/test_litellm/integrations/otel/test_otel_v2_logger.py index f9a61b689cb..34b55538dc3 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_logger.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_logger.py @@ -1623,17 +1623,21 @@ def test_provider_model_and_team_metadata_on_real_boundary_flow(): def test_pre_call_hook_seeds_baggage_onto_server_and_child_spans(): """The pre-call hook seeds identity Baggage in the request context so the server span (stamped directly) AND later child spans (service here, via the - Baggage processor) carry identity — not just the LLM-call span.""" + Baggage processor) carry identity — not just the LLM-call span. Only the + caller's ``requester_metadata`` is read from the request dict: the proxy's + own ``requester_ip_address`` stays unpromoted under the default allowlist.""" logger, exporter = _logger() server = logger._emitter.start_span( SpanRole.PROXY_REQUEST, LITELLM_PROXY_REQUEST_SPAN_NAME ) + data = { + "model": "gpt-4o", + "metadata": {"requester_ip_address": "127.0.0.1", "requester_metadata": {"trace_id": "abc"}}, + } async def _flow(): # pre-call seeds baggage + stamps the active server span - await logger.async_pre_call_hook( - _Auth(), None, {"model": "gpt-4o"}, "completion" - ) + await logger.async_pre_call_hook(_Auth(), None, data, "completion") # a later service call (same task) must inherit the identity await logger.async_service_success_hook( payload=_ServicePayload("redis", "set"), parent_otel_span=server @@ -1653,6 +1657,11 @@ def test_pre_call_hook_seeds_baggage_onto_server_and_child_spans(): srv.attributes[LiteLLM.TEAM_ID] == "t1" ) # stamped directly on the server span assert srv.attributes[f"{LiteLLM.METADATA_PREFIX}user_api_key_user_id"] == "u1" + assert not any( + k in (f"{LiteLLM.METADATA_PREFIX}requester_ip_address", f"{LiteLLM.METADATA_PREFIX}trace_id") + for s in (redis, srv) + for k in s.attributes + ) def test_pre_call_hook_promotes_nested_request_metadata_key():