mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(otel): read only the caller's requester_metadata snapshot in the v2 pre-call hook
The pre-call hook passed the proxy's whole per-request metadata dict into the request identity, so proxy-owned siblings such as requester_ip_address were promoted alongside the caller's keys. Only the requester_metadata mapping is read now, keyed under its wrapper, which keeps the default allowlist behaviour unchanged Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
5e2d9e1d5c
commit
30f02aa6da
2 changed files with 27 additions and 16 deletions
|
|
@ -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.<key>`` resolves too.
|
||||
promotes; ``request_metadata`` (the caller's ``requester_metadata``
|
||||
snapshot) is flattened to dotted keys so ``requester_metadata.<key>``
|
||||
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]]:
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue