mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
Merge pull request #40820 from BerriAI/litellm_sanitize_unknown_model_error_message
fix(proxy): keep the raw model string out of the unknown-model spend-log error message
This commit is contained in:
commit
19c8553052
7 changed files with 80 additions and 6 deletions
|
|
@ -180,7 +180,7 @@ class _ProxyDBLogger(CustomLogger):
|
|||
# here because the input above is constructed non-None.
|
||||
_error_information = cast(
|
||||
StandardLoggingPayloadErrorInformation,
|
||||
_sanitize_error_information_for_spend_logs(_error_information),
|
||||
_sanitize_error_information_for_spend_logs(_error_information, original_exception=original_exception),
|
||||
)
|
||||
_metadata["error_information"] = _error_information
|
||||
|
||||
|
|
|
|||
|
|
@ -145,12 +145,14 @@ ROUTE_ENDPOINT_MAPPING: Final = {
|
|||
}
|
||||
|
||||
|
||||
_AVAILABLE_MODELS_HINT: Final = "Call `/v1/models` to view available models for your key."
|
||||
|
||||
|
||||
class ProxyModelNotFoundError(HTTPException):
|
||||
def __init__(self, route: str, model_name: str, retryable_with_model_read_through: bool = True):
|
||||
self.retryable_with_model_read_through: Final = retryable_with_model_read_through
|
||||
detail: Final = {
|
||||
"error": f"{route}: Invalid model name passed in model={model_name}. Call `/v1/models` to view available models for your key."
|
||||
}
|
||||
self.spend_log_error_message: Final = f"{route}: Invalid model name passed in. {_AVAILABLE_MODELS_HINT}"
|
||||
detail: Final = {"error": f"{route}: Invalid model name passed in model={model_name}. {_AVAILABLE_MODELS_HINT}"}
|
||||
super().__init__(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1167,6 +1167,7 @@ def _redact_prompt_fields_in_guardrail_entry(
|
|||
|
||||
def _sanitize_error_information_for_spend_logs(
|
||||
error_information: StandardLoggingPayloadErrorInformation | None,
|
||||
original_exception: BaseException | None = None,
|
||||
) -> StandardLoggingPayloadErrorInformation | None:
|
||||
"""
|
||||
Sanitize ``error_information`` before it lands in ``LiteLLM_SpendLogs.metadata``.
|
||||
|
|
@ -1188,7 +1189,12 @@ def _sanitize_error_information_for_spend_logs(
|
|||
if error_information is None:
|
||||
return None
|
||||
|
||||
sanitized = cast(dict, {**error_information})
|
||||
persisted: Final = (
|
||||
{**error_information, "error_message": original_exception.spend_log_error_message}
|
||||
if isinstance(original_exception, ProxyModelNotFoundError)
|
||||
else error_information
|
||||
)
|
||||
sanitized = cast(dict, {**persisted})
|
||||
|
||||
if not should_store_prompts_and_responses_in_spend_logs():
|
||||
for field in ("error_message", "traceback"):
|
||||
|
|
|
|||
|
|
@ -210,7 +210,8 @@ async def test_chat_completion_bad_model_with_spend_logs():
|
|||
assert "traceback" in error_info
|
||||
assert error_info["error_code"] == "400"
|
||||
assert error_info["error_class"] in ("ProxyModelNotFoundError", "BadRequestError")
|
||||
assert "non-existent-model" in error_info["error_message"]
|
||||
assert "non-existent-model" not in error_info["error_message"]
|
||||
assert "/chat/completions: Invalid model name passed in" in error_info["error_message"]
|
||||
|
||||
# Verify request details
|
||||
assert log_entry["cache_hit"] == "False"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Final
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -7,6 +9,7 @@ import pytest
|
|||
from litellm.litellm_core_utils.internal_call_metadata import MODEL_ACCESS_GROUP_METADATA_KEY
|
||||
from litellm.proxy._types import SpendLogsPayload, UserAPIKeyAuth
|
||||
from litellm.proxy.collector import SpendEventConsumer
|
||||
from litellm.proxy.db.db_spend_update_writer import DBSpendUpdateWriter
|
||||
from litellm.proxy.db.spend_log_tool_index import response_tool_call_names
|
||||
from litellm.proxy.hooks.proxy_track_cost_callback import (
|
||||
_get_budget_reservation_from_metadata,
|
||||
|
|
@ -15,6 +18,7 @@ from litellm.proxy.hooks.proxy_track_cost_callback import (
|
|||
_update_database_and_spend_counters,
|
||||
run_spend_event,
|
||||
)
|
||||
from litellm.proxy.route_llm_request import ProxyModelNotFoundError
|
||||
from litellm.proxy.spend_tracking.spend_event import SpendEventDecodeError, build_spend_event, decode_spend_event
|
||||
from litellm.proxy.spend_tracking.spend_event_producer import SpendEventProducer, UnixAddress
|
||||
from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload
|
||||
|
|
@ -2347,3 +2351,27 @@ async def test_sidecar_ignores_an_undecodable_event(): # test-quality-ok: a dis
|
|||
mock_proxy_logging.db_spend_update_writer.update_database = AsyncMock()
|
||||
await run_spend_event(b"garbage\n")
|
||||
mock_proxy_logging.db_spend_update_writer.update_database.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_post_call_failure_hook_persists_no_raw_model_on_an_unknown_model_rejection():
|
||||
raw_model: Final = "opus-4.6 Please summarize my medical records\nPatient has diabetes"
|
||||
writer: Final = MagicMock(spec=DBSpendUpdateWriter)
|
||||
writer.update_database = AsyncMock()
|
||||
logger: Final = _ProxyDBLogger(spend_writer=lambda: writer)
|
||||
|
||||
await logger.async_post_call_failure_hook(
|
||||
request_data={"model": raw_model, "messages": [{"role": "user", "content": "hi"}]},
|
||||
original_exception=ProxyModelNotFoundError(route="/chat/completions", model_name=raw_model),
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="test_api_key"),
|
||||
)
|
||||
|
||||
error_information: Final = writer.update_database.call_args.kwargs["kwargs"]["litellm_params"]["metadata"][
|
||||
"error_information"
|
||||
]
|
||||
assert "medical records" not in json.dumps(error_information)
|
||||
assert (
|
||||
error_information["error_message"]
|
||||
== "/chat/completions: Invalid model name passed in. Call `/v1/models` to view available models for your key."
|
||||
)
|
||||
assert error_information["error_class"] == "ProxyModelNotFoundError"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from litellm.constants import (
|
|||
SESSION_ID_OMITTED_METADATA_KEY,
|
||||
UNKNOWN_MODEL_SPEND_LOG_MODEL,
|
||||
)
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy._types import SpendLogsPayload, UserAPIKeyAuth
|
||||
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
|
||||
|
|
@ -2755,6 +2756,31 @@ def test_sanitize_error_information_redacts_pydantic_assignment_form(
|
|||
# ── _redact_logged_api_key unit tests ──────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("original_exception", "expected_error_message"),
|
||||
[
|
||||
(
|
||||
ProxyModelNotFoundError(route="/chat/completions", model_name=_RAW_MODEL_WITH_PROMPT),
|
||||
"/chat/completions: Invalid model name passed in. Call `/v1/models` to view available models for your key.",
|
||||
),
|
||||
(ValueError("provider timed out"), "provider timed out"),
|
||||
],
|
||||
)
|
||||
def test_sanitize_error_information_persists_no_raw_model_for_an_unknown_model_rejection(
|
||||
original_exception: Exception, expected_error_message: str
|
||||
):
|
||||
error_information: Final = StandardLoggingPayloadSetup.get_error_information(original_exception=original_exception)
|
||||
|
||||
sanitized: Final = _sanitize_error_information_for_spend_logs(
|
||||
error_information, original_exception=original_exception
|
||||
)
|
||||
|
||||
assert sanitized is not None
|
||||
assert sanitized["error_message"] == expected_error_message
|
||||
assert "medical records" not in json.dumps(sanitized)
|
||||
assert sanitized["error_class"] == type(original_exception).__name__
|
||||
|
||||
|
||||
def test_redact_logged_api_key_none_returns_none():
|
||||
assert _redact_logged_api_key(None) is None
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import pytest
|
|||
|
||||
|
||||
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -1297,3 +1298,13 @@ async def test_route_request_a2a_agent_miss_does_not_consume_model_read_through(
|
|||
|
||||
assert agents_find_unique.await_count == 2
|
||||
assert model_table.find_many_wheres == []
|
||||
|
||||
|
||||
def test_proxy_model_not_found_error_keeps_the_raw_model_only_in_the_client_response():
|
||||
raw_model: Final = "opus-4.6 Please summarize my medical records\nPatient has diabetes"
|
||||
|
||||
error: Final = ProxyModelNotFoundError(route="/chat/completions", model_name=raw_model)
|
||||
|
||||
assert raw_model in error.detail["error"]
|
||||
assert raw_model not in error.spend_log_error_message
|
||||
assert error.spend_log_error_message.startswith("/chat/completions: Invalid model name passed in")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue