fix(spend_tracking): derive failure call_type from the requested route

An upstream failure never reaches the SDK, so nothing stamps call_type on the request data the failure hook logs from; use the route the caller hit, falling back to the request's Logging object for routes outside the map.
This commit is contained in:
Devin AI 2026-07-29 09:48:28 +00:00
parent fe42ca68a2
commit 4119cd67a1
2 changed files with 46 additions and 15 deletions

View file

@ -6,6 +6,9 @@ from typing import Any, List, Optional, Union, cast
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.api_route_to_call_types import (
get_primary_call_type_for_route,
)
from litellm.litellm_core_utils.core_helpers import (
_get_parent_otel_span_from_kwargs,
get_litellm_metadata_from_kwargs,
@ -148,6 +151,12 @@ class _ProxyDBLogger(CustomLogger):
request_data["litellm_params"]["proxy_server_request"] = (
request_data.get("proxy_server_request") or existing_litellm_params.get("proxy_server_request") or {}
)
# Failures that never reached the SDK carry no call_type, so the row lands
# without one and can't be attributed to the route the caller used.
route_call_type = get_primary_call_type_for_route(request_route)
if not request_data.get("call_type") and route_call_type is not None:
request_data["call_type"] = route_call_type.value
request_data[metadata_key] = spend_metadata
request_data["litellm_params"]["metadata"] = spend_metadata
if "litellm_metadata" in existing_litellm_params:
@ -174,8 +183,6 @@ class _ProxyDBLogger(CustomLogger):
)
if request_data.get("litellm_trace_id") is None:
request_data["litellm_trace_id"] = getattr(_litellm_logging_obj, "litellm_trace_id", None)
# Without this the spend log for a failure has a blank call_type, so the
# row can't be attributed to the route the caller actually used.
if not request_data.get("call_type"):
logged_call_type = _known_call_type(getattr(_litellm_logging_obj, "call_type", None))
if logged_call_type is not None:

View file

@ -1401,27 +1401,51 @@ async def test_failure_spend_log_drops_caller_supplied_key_identity():
@pytest.mark.asyncio
@pytest.mark.parametrize(
"logged_call_type, expected_call_type",
"request_route, expected_call_type",
[
("aresponses", "aresponses"),
("anthropic_messages", "anthropic_messages"),
("/v1/responses", ""),
("/v1/responses", "aresponses"),
("/v1/messages", "anthropic_messages"),
("/v1/chat/completions", "acompletion"),
],
)
async def test_failure_spend_log_call_type_comes_from_logging_object(
logged_call_type, expected_call_type
):
"""Regression for #35068: failure rows were written with a blank call_type, so a
failed Responses request was indistinguishable from a failed chat completion. Route
strings left on the Logging object by proxy-only errors must not leak into the
column."""
async def test_failure_spend_log_call_type_comes_from_the_requested_route(request_route, expected_call_type):
"""Regression for #35068: an upstream failure never reaches the SDK, so nothing sets
call_type and the row was written blank; a failed Responses request was then
indistinguishable from a failed chat completion."""
payload, _ = await _failure_spend_log_payload(
request_data={"model": "test-model", "litellm_metadata": {}},
user_api_key_dict=UserAPIKeyAuth(
api_key="sk-test",
user_id="real-user",
team_id="real-team",
request_route=request_route,
),
)
assert payload["call_type"] == expected_call_type
@pytest.mark.asyncio
async def test_failure_spend_log_call_type_falls_back_to_logging_object():
"""Routes outside the route map (e.g. pass-through) still know their call type from
the request's Logging object. Route strings left there by proxy-only errors must not
leak into the column."""
payload, _ = await _failure_spend_log_payload(
request_data={
"model": "test-model",
"litellm_metadata": {},
"litellm_logging_obj": _StubLoggingObj(call_type=logged_call_type),
"litellm_logging_obj": _StubLoggingObj(call_type="aresponses"),
},
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_id="real-user", team_id="real-team"),
)
assert payload["call_type"] == "aresponses"
assert payload["call_type"] == expected_call_type
payload, _ = await _failure_spend_log_payload(
request_data={
"model": "test-model",
"litellm_metadata": {},
"litellm_logging_obj": _StubLoggingObj(call_type="/v1/responses"),
},
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_id="real-user", team_id="real-team"),
)
assert payload["call_type"] == ""