mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
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:
commit
c5b0d6218d
4 changed files with 66 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue