fix(spend_tracking): resolve metadata.response_id from the same sources as the row key

A response without an id of its own is keyed on the id the standard logging payload
resolved, so the provider id kept in metadata follows that source too, stopping short of
the proxy's own call id, which is not a provider identity.
This commit is contained in:
Yucheng He 2026-09-15 11:46:48 -07:00
parent e7442c7232
commit 8344a96381
2 changed files with 62 additions and 16 deletions

View file

@ -230,6 +230,24 @@ def _get_spend_logs_metadata(
BATCH_COST_REQUEST_ID_SUFFIX: Final = "_batch_cost"
def get_provider_response_id(response_obj: Mapping[str, object], kwargs: Mapping[str, object]) -> str | None:
"""The id the provider minted for this response: the response's own, else the one the standard
logging payload resolved. Never the proxy's call id, which is not a provider identity."""
standard_logging_payload: Final = kwargs.get("standard_logging_object")
candidate_ids: Final = (
response_obj.get("id"),
standard_logging_payload.get("id") if isinstance(standard_logging_payload, dict) else None,
)
return next(
(
candidate
for candidate in candidate_ids
if isinstance(candidate, str) and candidate and candidate != kwargs.get("litellm_call_id")
),
None,
)
def get_spend_logs_id(call_type: str, response_obj: dict, kwargs: dict) -> str | None:
standard_logging_payload = kwargs.get("standard_logging_object")
candidate_ids: Final = (
@ -394,7 +412,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs
usage = _combined_usage.model_dump()
id = get_spend_logs_id(call_type or "acompletion", response_obj_dict, kwargs)
raw_response_id: Final = response_obj_dict.get("id")
provider_response_id: Final = get_provider_response_id(response_obj_dict, kwargs)
standard_logging_payload: Final = cast(StandardLoggingPayload | None, kwargs.get("standard_logging_object", None))
end_user_id = get_end_user_id_for_cost_tracking(litellm_params)
@ -530,7 +548,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs
standard_logging_payload.get("autorouter_savings", None) if standard_logging_payload is not None else None
),
litellm_call_id=litellm_call_id,
response_id=raw_response_id if isinstance(raw_response_id, str) else None,
response_id=provider_response_id,
router_metadata=_get_router_metadata_for_spend_log(
metadata=metadata,
requested_model=_model_group,

View file

@ -79,10 +79,16 @@ def test_classifier_audit_spend_storage_obeys_privacy_and_truncation(monkeypatch
"classifier_input": {"system": "rubric" * 1000, "messages": [{"role": "user", "content": "ask"}]},
"originating_request_masked": {"input": "source-only", "api_key": "REDACTED"},
}
stored: Final = json.loads(_get_proxy_server_request_for_spend_logs_payload(
metadata={}, litellm_params={"proxy_server_request": {"body": {"model": "classifier"}}},
kwargs={"standard_logging_object": audit, "standard_callback_dynamic_params": {"turn_off_message_logging": redact}},
))
stored: Final = json.loads(
_get_proxy_server_request_for_spend_logs_payload(
metadata={},
litellm_params={"proxy_server_request": {"body": {"model": "classifier"}}},
kwargs={
"standard_logging_object": audit,
"standard_callback_dynamic_params": {"turn_off_message_logging": redact},
},
)
)
if not store_prompts or redact:
assert "classifier_input" not in stored
assert "originating_request_masked" not in stored
@ -208,9 +214,7 @@ def test_batch_lifecycle_rows_derive_the_same_session_from_the_batch_id():
from litellm.proxy.spend_tracking.spend_tracking_utils import _get_batch_trace_session_id
create_session: Final = _get_batch_trace_session_id(call_type="acreate_batch", request_id="batch-uid-1")
cost_session: Final = _get_batch_trace_session_id(
call_type="aretrieve_batch", request_id="batch-uid-1_batch_cost"
)
cost_session: Final = _get_batch_trace_session_id(call_type="aretrieve_batch", request_id="batch-uid-1_batch_cost")
assert create_session == cost_session == "batch-uid-1"
@ -1393,6 +1397,34 @@ def test_get_logging_payload_leaves_metadata_response_id_empty_without_a_respons
assert json.loads(payload["metadata"])["response_id"] is None
@pytest.mark.parametrize(
"standard_logging_id, expected",
[("provider-123", "provider-123"), ("call-1", None), (None, None)],
)
def test_get_logging_payload_falls_back_to_the_standard_logging_payload_response_id(
standard_logging_id: str | None, expected: str | None
):
"""A response without an id of its own is keyed on the id the standard logging payload resolved,
so metadata.response_id follows the same source, except that the proxy's own call id, which
that payload falls back to, is not a provider identity."""
standard_logging_payload = _make_standard_logging_payload_with_usage_object({})
standard_logging_payload["id"] = standard_logging_id
payload = get_logging_payload(
kwargs={
"model": "gpt-4o-mini",
"litellm_call_id": "call-1",
"litellm_params": {"metadata": {"user_api_key": "test-key"}},
"standard_logging_object": standard_logging_payload,
},
response_obj={"choices": []},
start_time=datetime.datetime.now(timezone.utc),
end_time=datetime.datetime.now(timezone.utc),
)
assert json.loads(payload["metadata"])["response_id"] == expected
assert payload["request_id"] == (standard_logging_id or "call-1")
@patch("litellm.proxy.proxy_server.master_key", None)
@patch("litellm.proxy.proxy_server.general_settings", {})
def test_get_logging_payload_includes_overhead_in_spend_logs_metadata():
@ -4497,7 +4529,7 @@ ANTHROPIC_MESSAGES_SSE_CHUNKS: Final = (
'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n',
'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn"},'
'"usage":{"output_tokens":4}}\n\n',
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
'event: message_stop\ndata: {"type":"message_stop"}\n\n',
)
@ -4535,9 +4567,7 @@ def test_spend_log_request_id_is_the_message_id_a_non_streaming_messages_caller_
"""
logging_obj = _anthropic_messages_logging_obj(stream=False)
logged_response = logging_obj._handle_anthropic_messages_response_logging(
result=ANTHROPIC_MESSAGES_RESPONSE
)
logged_response = logging_obj._handle_anthropic_messages_response_logging(result=ANTHROPIC_MESSAGES_RESPONSE)
assert logged_response.id == "msg_01Lit6806NonStreaming"
assert (
@ -4613,9 +4643,7 @@ def test_spend_log_request_id_still_falls_back_to_litellm_call_id_without_a_prov
end_time=datetime.datetime.now(timezone.utc),
logging_obj=logging_obj,
)
assert logging_obj.model_call_details["complete_streaming_response"].id == (
"6806cafe-0000-4000-8000-000000000001"
)
assert logging_obj.model_call_details["complete_streaming_response"].id == ("6806cafe-0000-4000-8000-000000000001")
def test_spend_log_request_id_for_chat_completions_is_untouched():