fix(logging): fall back to litellm_metadata when metadata is empty (#36105)

get_litellm_params returned metadata=None whenever only litellm_metadata was
supplied, which overwrote the fallback function_setup had already applied and
left litellm_params["metadata"] empty. On the /v1/responses
completion-transformation bridge, used by every provider without a native
Responses API config, and on /v1/messages, that discarded the caller's trace
fields a second time after the proxy had promoted them.

Resolve metadata to a copy of litellm_metadata when metadata is empty, guarding
on isinstance because the proxy leaves an unparseable litellm_metadata string in
place and a null metadata would otherwise suppress the backfill and break the
merge. update_from_kwargs copies rather than aliases for the same reason: on
these routes it is handed the caller's provider-bound dict and would otherwise
write user_api_key_auth into it.
This commit is contained in:
yucheng-berri 2026-08-06 17:32:20 -07:00 • committed by GitHub
parent f4f59ec4c3
commit f3f72c4574
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 80 additions and 4 deletions

View file

@ -115,8 +115,11 @@ def get_litellm_params(
litellm_request_debug: bool | None = None,
**kwargs,
) -> dict:
_litellm_metadata_dict: Final = litellm_metadata if isinstance(litellm_metadata, dict) else None
resolved_metadata: Final = _litellm_metadata_dict.copy() if not metadata and _litellm_metadata_dict else metadata
# Derive litellm_session_id / litellm_trace_id from metadata when not provided (call chaining)
_meta: Final = metadata or {}
_meta: Final = resolved_metadata or {}
if litellm_session_id is None:
litellm_session_id = _meta.get("session_id") or _meta.get("trace_id")
if litellm_trace_id is None:
@ -139,7 +142,7 @@ def get_litellm_params(
"model_alias_map": model_alias_map,
"completion_call_id": completion_call_id,
"aembedding": aembedding,
"metadata": metadata,
"metadata": resolved_metadata,
"model_info": model_info,
"proxy_server_request": proxy_server_request,
"preset_cache_key": preset_cache_key,

View file

@ -585,8 +585,8 @@ class Logging(LiteLLMLoggingBaseClass):
"""
base_litellm_params: Final[dict[str, Any]] = {}
if "metadata" in kwargs:
base_litellm_params["metadata"] = kwargs["metadata"]
if isinstance(kwargs.get("metadata"), dict):
base_litellm_params["metadata"] = kwargs["metadata"].copy()
if "litellm_metadata" in kwargs and isinstance(kwargs["litellm_metadata"], dict):
base_litellm_params["litellm_metadata"] = kwargs["litellm_metadata"]
if "metadata" not in base_litellm_params:

View file

@ -162,3 +162,56 @@ class TestGetLitellmParamsDataResidency:
api_base="https://eu.api.openai.com/v1",
)
assert result["data_residency"] is None
class TestMetadataFallsBackToLitellmMetadata:
def test_metadata_falls_back_to_litellm_metadata_when_absent(self):
result = get_litellm_params(litellm_metadata={"trace_id": "trace-1"})
assert result["metadata"] == {"trace_id": "trace-1"}
assert result["litellm_metadata"] == {"trace_id": "trace-1"}
def test_empty_metadata_falls_back_to_litellm_metadata(self):
result = get_litellm_params(metadata={}, litellm_metadata={"trace_id": "trace-1"})
assert result["metadata"] == {"trace_id": "trace-1"}
def test_metadata_wins_when_both_present(self):
result = get_litellm_params(
metadata={"trace_id": "from-metadata"},
litellm_metadata={"trace_id": "from-litellm-metadata"},
)
assert result["metadata"] == {"trace_id": "from-metadata"}
@pytest.mark.parametrize("bad_value", ["not-json-a-string", 12345, ["a"], True])
def test_non_dict_litellm_metadata_is_ignored(self, bad_value):
result = get_litellm_params(litellm_metadata=bad_value)
assert result["metadata"] is None
def test_metadata_stays_none_without_litellm_metadata(self):
result = get_litellm_params(api_key="test-key")
assert result["metadata"] is None
def test_session_and_trace_id_derived_from_litellm_metadata(self):
result = get_litellm_params(
litellm_metadata={"trace_id": "trace-1", "session_id": "session-1"},
)
assert result["litellm_session_id"] == "session-1"
assert result["litellm_trace_id"] == "trace-1"
def test_explicit_session_and_trace_id_are_not_overridden(self):
result = get_litellm_params(
litellm_session_id="explicit-session",
litellm_trace_id="explicit-trace",
litellm_metadata={"trace_id": "trace-1", "session_id": "session-1"},
)
assert result["litellm_session_id"] == "explicit-session"
assert result["litellm_trace_id"] == "explicit-trace"
def test_litellm_metadata_fallback_is_copied_not_aliased(self):
litellm_metadata = {"trace_id": "trace-1"}
result = get_litellm_params(litellm_metadata=litellm_metadata)
assert result["metadata"] == litellm_metadata
assert result["metadata"] is not litellm_metadata
result["metadata"].pop("trace_id")
assert litellm_metadata == {"trace_id": "trace-1"}

View file

@ -526,6 +526,26 @@ class TestUpdateFromKwargs:
)
assert logging_obj.litellm_params["litellm_call_id"] == "call-empty"
@pytest.mark.parametrize("caller_metadata", [None, "not-a-dict", 42])
def test_non_dict_caller_metadata_does_not_break_the_merge(self, logging_obj, caller_metadata):
logging_obj.update_from_kwargs(
kwargs={"metadata": caller_metadata, "litellm_metadata": {"user_api_key_hash": "hashed"}},
litellm_params={"metadata": {"user_api_key_hash": "hashed", "litellm_api_version": "1.0"}},
)
assert logging_obj.litellm_params["metadata"]["user_api_key_hash"] == "hashed"
def test_does_not_mutate_caller_metadata_dict(self, logging_obj):
caller_metadata: dict = {}
logging_obj.update_from_kwargs(
kwargs={"metadata": caller_metadata, "litellm_metadata": {"user_api_key_hash": "hashed"}},
litellm_params={"metadata": {"user_api_key_hash": "hashed", "litellm_api_version": "1.0"}},
)
assert caller_metadata == {}
assert logging_obj.litellm_params["metadata"]["user_api_key_hash"] == "hashed"
def test_logging_prevent_double_logging(logging_obj):
"""