mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge 4d3940cdb6 into b781d157d7
This commit is contained in:
commit
ff17a5b725
3 changed files with 1782 additions and 15 deletions
|
|
@ -150,6 +150,19 @@ def _session_id_from_baggage(baggage: str) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def _caller_trace_field(data: Mapping[str, object], metadata_variable_name: str, field: str) -> object | None:
|
||||
active: Final = data.get(metadata_variable_name)
|
||||
if isinstance(active, Mapping) and field in active:
|
||||
active_map: Final = cast(Mapping[str, object], active) # cast-ok: isinstance above, free-form JSON values
|
||||
return active_map[field] or None
|
||||
promoted: Final = metadata_variable_name == "litellm_metadata" and field in LITELLM_TRACE_CONTROL_METADATA_FIELDS
|
||||
requester: Final = data.get("metadata")
|
||||
if not promoted or not isinstance(requester, Mapping):
|
||||
return None
|
||||
requester_map: Final = cast(Mapping[str, object], requester) # cast-ok: isinstance above, free-form JSON values
|
||||
return requester_map.get(field) or None
|
||||
|
||||
|
||||
def _stampable_key_hash(user_api_key_dict: UserAPIKeyAuth) -> str | None:
|
||||
"""Only proxy-validated keys are stamped, proven by the unforgeable
|
||||
via_virtual_key marker AND a known non-secret shape: the sha256 hex digest
|
||||
|
|
@ -828,11 +841,15 @@ def apply_missing_session_id_policy(
|
|||
):
|
||||
metadata["session_id"] = body_session_id
|
||||
return
|
||||
if data.get("litellm_session_id") or metadata.get("session_id"):
|
||||
if data.get("litellm_session_id") or _caller_trace_field(data, _metadata_variable_name, "session_id") is not None:
|
||||
return
|
||||
match policy:
|
||||
case "generate":
|
||||
session_id: Final = str(data.get("litellm_trace_id") or metadata.get("trace_id") or uuid.uuid4())
|
||||
session_id: Final = str(
|
||||
data.get("litellm_trace_id")
|
||||
or _caller_trace_field(data, _metadata_variable_name, "trace_id")
|
||||
or uuid.uuid4()
|
||||
)
|
||||
data["litellm_session_id"] = session_id # rebind-ok: data is an out-param
|
||||
data.setdefault("litellm_trace_id", session_id)
|
||||
metadata["session_id"] = session_id
|
||||
|
|
@ -1579,12 +1596,21 @@ class LiteLLMProxyRequestSetup:
|
|||
# Last-resort fallback: the W3C standards for trace/session propagation
|
||||
# (https://www.w3.org/TR/trace-context/, https://www.w3.org/TR/baggage/).
|
||||
# Lower priority than everything above - only fires when neither the
|
||||
# explicit litellm headers nor the Anthropic-metadata path found
|
||||
# anything - but lets a caller's existing traceparent/baggage headers
|
||||
# (from real OTel instrumentation) correlate with litellm's own logs
|
||||
# instead of generating an unrelated trace_id.
|
||||
# explicit litellm headers, the Anthropic-metadata path, nor the
|
||||
# caller's own request metadata set the field - but lets a caller's
|
||||
# existing traceparent/baggage headers (from real OTel instrumentation)
|
||||
# correlate with litellm's own logs instead of generating an unrelated
|
||||
# trace_id.
|
||||
normalized_headers: Final = MappingProxyType({k.lower(): v for k, v in headers.items() if isinstance(k, str)})
|
||||
if "litellm_trace_id" not in data:
|
||||
if (
|
||||
"litellm_trace_id" not in data
|
||||
and _caller_trace_field(
|
||||
cast(Mapping[str, object], data), # cast-ok: request body is a str-keyed JSON object
|
||||
_metadata_variable_name,
|
||||
"trace_id",
|
||||
)
|
||||
is None
|
||||
):
|
||||
traceparent: Final = normalized_headers.get("traceparent")
|
||||
if isinstance(traceparent, str):
|
||||
trace_id_from_traceparent: Final = _trace_id_from_traceparent(traceparent)
|
||||
|
|
@ -1594,7 +1620,15 @@ class LiteLLMProxyRequestSetup:
|
|||
verbose_proxy_logger.debug(
|
||||
"Extracted trace_id from W3C traceparent header: %s", trace_id_from_traceparent
|
||||
)
|
||||
if "litellm_session_id" not in data:
|
||||
if (
|
||||
"litellm_session_id" not in data
|
||||
and _caller_trace_field(
|
||||
cast(Mapping[str, object], data), # cast-ok: request body is a str-keyed JSON object
|
||||
_metadata_variable_name,
|
||||
"session_id",
|
||||
)
|
||||
is None
|
||||
):
|
||||
baggage: Final = normalized_headers.get("baggage")
|
||||
if isinstance(baggage, str):
|
||||
session_id_from_baggage: Final = _session_id_from_baggage(baggage)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3673,6 +3673,76 @@ def test_add_litellm_metadata_from_request_headers_explicit_trace_id_beats_trace
|
|||
assert data["litellm_session_id"] == "explicit-trace-id-value"
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_request_headers_body_trace_id_beats_traceparent():
|
||||
headers = {"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}
|
||||
data = {"metadata": {"trace_id": "caller-chosen-trace-id"}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers=headers, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data["metadata"]["trace_id"] == "caller-chosen-trace-id"
|
||||
assert "litellm_trace_id" not in data
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_request_headers_body_session_id_beats_baggage():
|
||||
headers = {"baggage": "session.id=baggage-session-42"}
|
||||
data = {"metadata": {"session_id": "caller-chosen-session-id"}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers=headers, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data["metadata"]["session_id"] == "caller-chosen-session-id"
|
||||
assert "litellm_session_id" not in data
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_request_headers_body_steering_is_per_field():
|
||||
headers = {
|
||||
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
||||
"baggage": "session.id=baggage-session-42",
|
||||
}
|
||||
data = {"metadata": {"trace_id": "caller-chosen-trace-id"}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers=headers, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data["metadata"]["trace_id"] == "caller-chosen-trace-id"
|
||||
assert data["litellm_session_id"] == "baggage-session-42"
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_request_headers_litellm_metadata_steering_honoured():
|
||||
headers = {"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}
|
||||
data = {"litellm_metadata": {"trace_id": "caller-chosen-trace-id"}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers=headers, data=data, _metadata_variable_name="litellm_metadata"
|
||||
)
|
||||
assert data["litellm_metadata"]["trace_id"] == "caller-chosen-trace-id"
|
||||
assert "litellm_trace_id" not in data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("empty_session_id", ["", None])
|
||||
def test_add_litellm_metadata_from_request_headers_empty_body_session_id_falls_back_to_baggage(
|
||||
empty_session_id: str | None,
|
||||
):
|
||||
headers = {"baggage": "session.id=baggage-session-42"}
|
||||
data = {"metadata": {"session_id": empty_session_id}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers=headers, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data["litellm_session_id"] == "baggage-session-42"
|
||||
assert data["metadata"]["session_id"] == "baggage-session-42"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["trace_id", "session_id"])
|
||||
def test_add_litellm_metadata_from_request_headers_promoted_metadata_beats_headers(field: str):
|
||||
headers = {
|
||||
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
||||
"baggage": "session.id=baggage-session-42",
|
||||
}
|
||||
data = {"metadata": {field: "caller-chosen"}, "litellm_metadata": {}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers=headers, data=data, _metadata_variable_name="litellm_metadata"
|
||||
)
|
||||
assert field not in data["litellm_metadata"]
|
||||
assert f"litellm_{field}" not in data
|
||||
|
||||
|
||||
def _otel_span_with_trace_id(trace_id: int) -> NonRecordingSpan:
|
||||
return NonRecordingSpan(SpanContext(trace_id=trace_id, span_id=0x00F067AA0BA902B7, is_remote=False))
|
||||
|
||||
|
|
@ -8155,6 +8225,85 @@ async def test_missing_session_id_omit_keeps_client_supplied_session_id():
|
|||
assert _spend_log_session_id(updated) == "client-session-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("path", "client_body"),
|
||||
[
|
||||
("/v1/chat/completions", {"model": "gpt-4o", "messages": [], "metadata": {"session_id": ""}}),
|
||||
("/v1/responses", {"model": "gpt-4o", "input": "hi"}),
|
||||
],
|
||||
)
|
||||
async def test_missing_session_id_reject_accepts_baggage_session_id(path: str, client_body: dict[str, object]):
|
||||
request = _request_for(path)
|
||||
request.headers = {"baggage": "session.id=baggage-session-42"}
|
||||
|
||||
updated = await add_litellm_data_to_request(
|
||||
data=client_body,
|
||||
request=request,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key"),
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={"missing_session_id": "reject"},
|
||||
)
|
||||
|
||||
assert updated["litellm_session_id"] == "baggage-session-42"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("path", ["/v1/responses", "/v1/messages"])
|
||||
@pytest.mark.parametrize("policy", [None, "reject", "generate"])
|
||||
async def test_promoted_caller_trace_ids_beat_traceparent_and_baggage(path: str, policy: str | None):
|
||||
request = _request_for(path)
|
||||
request.headers = {
|
||||
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
||||
"baggage": "session.id=baggage-session-42",
|
||||
}
|
||||
|
||||
updated = await add_litellm_data_to_request(
|
||||
data={"model": "gpt-4o", "metadata": {"trace_id": "caller-trace", "session_id": "caller-session"}},
|
||||
request=request,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key"),
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={"missing_session_id": policy} if policy else {},
|
||||
)
|
||||
|
||||
assert updated["litellm_metadata"]["trace_id"] == "caller-trace"
|
||||
assert updated["litellm_metadata"]["session_id"] == "caller-session"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_session_id_reject_ignores_requester_session_id_shadowed_by_empty_litellm_metadata():
|
||||
with pytest.raises(ProxyException) as exc_info:
|
||||
await add_litellm_data_to_request(
|
||||
data={
|
||||
"model": "gpt-4o",
|
||||
"input": "hi",
|
||||
"metadata": {"session_id": "caller-session"},
|
||||
"litellm_metadata": {"session_id": ""},
|
||||
},
|
||||
request=_request_for("/v1/responses"),
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key"),
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={"missing_session_id": "reject"},
|
||||
)
|
||||
assert exc_info.value.code == "400"
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_request_headers_empty_litellm_metadata_field_falls_back_to_headers():
|
||||
headers = {
|
||||
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
||||
"baggage": "session.id=baggage-session-42",
|
||||
}
|
||||
data = {
|
||||
"metadata": {"trace_id": "caller-trace", "session_id": "caller-session"},
|
||||
"litellm_metadata": {"trace_id": "", "session_id": ""},
|
||||
}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers=headers, data=data, _metadata_variable_name="litellm_metadata"
|
||||
)
|
||||
assert data["litellm_trace_id"] == "4bf92f3577b34da6a3ce929d0e0e4736"
|
||||
assert data["litellm_session_id"] == "baggage-session-42"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"client_body",
|
||||
|
|
@ -8274,6 +8423,39 @@ async def test_missing_session_id_generate_reuses_traceparent_trace_id():
|
|||
assert _spend_log_session_id(updated) == "4bf92f3577b34da6a3ce929d0e0e4736"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("path", ["/v1/responses", "/v1/messages"])
|
||||
@pytest.mark.parametrize(
|
||||
"headers",
|
||||
[
|
||||
{"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"},
|
||||
{},
|
||||
],
|
||||
ids=["with_traceparent", "no_traceparent"],
|
||||
)
|
||||
async def test_missing_session_id_generate_reuses_promoted_caller_trace_id(path: str, headers: dict[str, str]):
|
||||
request = _request_for(path)
|
||||
request.headers = headers
|
||||
|
||||
updated = await add_litellm_data_to_request(
|
||||
data={"model": "gpt-4o", "input": "hi", "metadata": {"trace_id": "caller-trace"}},
|
||||
request=request,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key"),
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={"missing_session_id": "generate"},
|
||||
)
|
||||
|
||||
caller_trace_msg: Final = "generate must derive the session from the caller's un-promoted metadata.trace_id"
|
||||
assert updated["litellm_session_id"] == "caller-trace", caller_trace_msg
|
||||
assert updated["litellm_metadata"]["session_id"] == "caller-trace", caller_trace_msg
|
||||
assert updated["litellm_metadata"][SESSION_ID_GENERATED_METADATA_KEY] is True, (
|
||||
"the derived session id must still be marked generated"
|
||||
)
|
||||
assert _spend_log_session_id(updated, "litellm_metadata") == "caller-trace", (
|
||||
"spend log and callback session ids must agree on the caller trace id"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("policy", ["generate", "reject"])
|
||||
async def test_missing_session_id_policy_keeps_client_supplied_session_id(policy: str):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue