Merge pull request #41633 from BerriAI/litellm_non_string_model_spend_tracking

fix(proxy): reject non-string model with 400 and log its spend as unknown-model
This commit is contained in:
kerry-berri 2026-09-17 12:49:23 -07:00 committed by GitHub
commit c5b0d6218d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 5 deletions

View file

@ -1937,6 +1937,14 @@ class ProxyBaseLLMRequestProcessing:
) -> tuple[dict, LiteLLMLoggingObj]:
start_time: Final = datetime.now() # start before calling guardrail hooks
requested_model: Final = self.data.get("model")
if requested_model is not None and not isinstance(requested_model, str):
raise ProxyException(
message="'model' must be a string.",
type=ProxyErrorTypes.bad_request_error,
param="model",
code=status.HTTP_400_BAD_REQUEST,
)
self.data = await add_litellm_data_to_request(
data=self.data,
request=request,

View file

@ -485,10 +485,13 @@ def get_logging_payload(
or None
)
custom_llm_provider: Final = logged_provider or _model_group_provider(_model_group, llm_router)
raw_model: Final = cast(str, kwargs.get("model") or "")
resolved_model: Final = (
standard_logging_payload.get("model") if standard_logging_payload is not None else None
) or reconstruct_model_name(raw_model, logged_provider, metadata or {})
requested_model: Final = cast(object, kwargs.get("model"))
raw_model: Final = requested_model if isinstance(requested_model, str) else ""
model_is_malformed: Final = requested_model is not None and not isinstance(requested_model, str)
logged_model: Final = standard_logging_payload.get("model") if standard_logging_payload is not None else None
resolved_model: Final = (logged_model if isinstance(logged_model, str) else None) or reconstruct_model_name(
raw_model, logged_provider, metadata or {}
)
failed_with_prompt_shaped_model: Final = (
_get_status_for_spend_log(metadata=metadata) == "failure"
and not _model_group
@ -496,7 +499,7 @@ def get_logging_payload(
)
model_name: Final = (
UNKNOWN_MODEL_SPEND_LOG_MODEL
if rejected_as_unknown_model or failed_with_prompt_shaped_model
if rejected_as_unknown_model or failed_with_prompt_shaped_model or model_is_malformed
else resolved_model
)
litellm_call_id: Final = cast(

View file

@ -1049,6 +1049,27 @@ def test_get_logging_payload_replaces_rejected_or_prompt_shaped_models_with_the_
assert payload["model"] == expected_model
@pytest.mark.parametrize("requested_model", [{"bad": "value"}, ["gpt-5.2"], 1])
def test_get_logging_payload_replaces_a_non_string_model_with_the_placeholder(
requested_model: dict[str, str] | list[str] | int,
):
kwargs: Final = {
"model": requested_model,
"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=ValueError("model must be a string"),
start_time=datetime.datetime.now(timezone.utc),
end_time=datetime.datetime.now(timezone.utc),
)
assert payload["model"] == UNKNOWN_MODEL_SPEND_LOG_MODEL
@pytest.mark.parametrize(
("metadata", "response_obj"),
[

View file

@ -327,6 +327,35 @@ class TestProxyBaseLLMRequestProcessing:
pytest.fail("litellm_call_id is not a valid UUID")
assert data_passed["litellm_call_id"] == returned_data["litellm_call_id"]
@pytest.mark.asyncio
@pytest.mark.parametrize("requested_model", [{"bad": "value"}, ["gpt-5.2"], 1])
async def test_common_processing_pre_call_logic_rejects_a_non_string_model_with_400(
self, monkeypatch, requested_model: dict[str, str] | list[str] | int
):
processing_obj = ProxyBaseLLMRequestProcessing(
data={"model": requested_model, "messages": [{"role": "user", "content": "hi"}]}
)
mock_request = MagicMock(spec=Request)
mock_request.headers = {}
add_litellm_data_to_request = AsyncMock()
monkeypatch.setattr(
litellm.proxy.common_request_processing, "add_litellm_data_to_request", add_litellm_data_to_request
)
with pytest.raises(ProxyException) as exc_info:
await processing_obj.common_processing_pre_call_logic(
request=mock_request,
general_settings={},
user_api_key_dict=MagicMock(spec=UserAPIKeyAuth),
proxy_logging_obj=MagicMock(spec=ProxyLogging),
proxy_config=MagicMock(spec=ProxyConfig),
route_type="acompletion",
)
assert exc_info.value.code == str(status.HTTP_400_BAD_REQUEST)
assert exc_info.value.param == "model"
add_litellm_data_to_request.assert_not_awaited()
@pytest.mark.asyncio
async def test_common_processing_pre_call_logic_refreshes_proxy_server_request_body_after_guardrails(
self, monkeypatch