From fe6f615c7add75fa5c54c2a33d15443aab391d25 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:18:54 -0700 Subject: [PATCH] fix(proxy): log rejected unknown-model requests under a placeholder model name A request whose model field matched no configured model was rejected with 400 but its failure row still persisted the raw client string as the model, so a client that concatenated its prompt into the model field wrote that prompt into LiteLLM_SpendLogs and the daily spend tables, where /user/daily/activity/aggregated returned it as a breakdown.models key. The spend log payload now records such rejections under the constant unknown-model, keeping the failed request counted without persisting client input as a model name. --- litellm/constants.py | 2 ++ .../spend_tracking/spend_tracking_utils.py | 10 ++++-- .../test_spend_tracking_utils.py | 35 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 0a7ef363e92..33a46616c4e 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1999,6 +1999,8 @@ NON_INFERENCE_CALL_TYPES: Final[frozenset[str]] = frozenset( } ) +UNKNOWN_MODEL_SPEND_LOG_MODEL: Final[str] = "unknown-model" + # PTU reservation rollup writes rows to LiteLLM_DailyTeamSpend with this # sentinel api_key so PTU flat cost stays distinguishable from real per-request # spend under the table's composite unique constraint. diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index a21d761996f..ebe5b530e60 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -18,6 +18,7 @@ from litellm.constants import ( LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME, REDACTED_BY_LITELM_STRING, SESSION_ID_OMITTED_METADATA_KEY, + UNKNOWN_MODEL_SPEND_LOG_MODEL, ) from litellm.constants import ( MAX_STRING_LENGTH_PROMPT_IN_DB as DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB, @@ -34,6 +35,7 @@ from litellm.litellm_core_utils.litellm_logging import ( ) from litellm.litellm_core_utils.safe_json_dumps import safe_dumps, strip_null_bytes from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload, SpendLogsRouterMetadata +from litellm.proxy.route_llm_request import ProxyModelNotFoundError from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error from litellm.proxy.utils import PrismaClient, hash_token from litellm.types.utils import ( @@ -335,6 +337,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs if kwargs is None: kwargs = {} + rejected_as_unknown_model: Final = isinstance(response_obj, ProxyModelNotFoundError) if response_obj is None: response_obj = {} elif not isinstance(response_obj, BaseModel) and not isinstance(response_obj, dict): @@ -433,8 +436,11 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs ) raw_model: Final = cast(str, kwargs.get("model") or "") model_name: Final = ( - standard_logging_payload.get("model") if standard_logging_payload is not None else None - ) or reconstruct_model_name(raw_model, custom_llm_provider, metadata or {}) + UNKNOWN_MODEL_SPEND_LOG_MODEL + if rejected_as_unknown_model + else (standard_logging_payload.get("model") if standard_logging_payload is not None else None) + or reconstruct_model_name(raw_model, custom_llm_provider, metadata or {}) + ) litellm_call_id: Final = cast( str | None, kwargs.get("litellm_call_id") or litellm_params.get("litellm_call_id"), 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 5a79560b972..7a20b9ab026 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 @@ -17,10 +17,12 @@ from litellm.constants import ( LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME, REDACTED_BY_LITELM_STRING, SESSION_ID_OMITTED_METADATA_KEY, + UNKNOWN_MODEL_SPEND_LOG_MODEL, ) from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup +from litellm.proxy.route_llm_request import ProxyModelNotFoundError from litellm.proxy.spend_tracking.spend_tracking_utils import ( _get_messages_for_spend_logs_payload, _get_proxy_server_request_for_spend_logs_payload, @@ -922,6 +924,39 @@ def test_safe_dumps_complex_metadata_like_object(): assert parsed["model"] == "gpt-4" +_RAW_MODEL_WITH_PROMPT: Final = "opus-4.6 Please summarize my medical records\nPatient has diabetes" + + +@pytest.mark.parametrize( + ("rejection", "expected_model"), + [ + ( + ProxyModelNotFoundError(route="acompletion", model_name=_RAW_MODEL_WITH_PROMPT), + UNKNOWN_MODEL_SPEND_LOG_MODEL, + ), + (ValueError("provider timed out"), _RAW_MODEL_WITH_PROMPT), + ], +) +def test_get_logging_payload_replaces_model_only_when_router_rejected_it_as_unknown( + rejection: Exception, expected_model: str +): + kwargs: Final = { + "model": _RAW_MODEL_WITH_PROMPT, + "messages": [{"role": "user", "content": "hi"}], + "call_type": "acompletion", + "litellm_params": {"metadata": {"user_api_key": "sk-test", "status": "failure"}}, + } + + payload: Final = get_logging_payload( + kwargs=kwargs, + response_obj=rejection, + start_time=datetime.datetime.now(timezone.utc), + end_time=datetime.datetime.now(timezone.utc), + ) + + assert payload["model"] == expected_model + + @patch("litellm.proxy.proxy_server.master_key", None) @patch("litellm.proxy.proxy_server.general_settings", {}) def test_get_logging_payload_api_key_preserved_when_standard_logging_payload_is_none():