diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 40039f640fc..6e5dcb0303d 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -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, diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py index a0b6d4f6bc6..e3eb08792fe 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py @@ -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():