mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
Merge pull request #41950 from BerriAI/litellm_cost_callback_bounded_error_msg
fix(proxy): keep request metadata out of the cost tracking failure alert
This commit is contained in:
commit
43e835c3ca
2 changed files with 102 additions and 9 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import asyncio
|
||||
import traceback
|
||||
from collections.abc import Callable, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
|
|
@ -446,17 +446,26 @@ class _ProxyDBLogger(CustomLogger):
|
|||
f"Cost tracking failed for model={model}.\nDebug info - {cost_tracking_failure_debug_info}\nAdd custom pricing - https://docs.litellm.ai/docs/proxy/custom_pricing"
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = f"Error in tracking cost callback - {e}\n Traceback:{traceback.format_exc()}"
|
||||
model = kwargs.get("model", "")
|
||||
metadata = get_litellm_metadata_from_kwargs(kwargs=kwargs)
|
||||
litellm_metadata: Final = kwargs.get("litellm_params", {}).get("litellm_metadata", {})
|
||||
old_metadata: Final = kwargs.get("litellm_params", {}).get("metadata", {})
|
||||
call_type = kwargs.get("call_type", "")
|
||||
error_msg += f"\n Args to _PROXY_track_cost_callback\n model: {model}\n chosen_metadata: {metadata}\n litellm_metadata: {litellm_metadata}\n old_metadata: {old_metadata}\n call_type: {call_type}\n"
|
||||
failing_model: Final = kwargs.get("model", "")
|
||||
failing_call_type: Final = kwargs.get("call_type", "")
|
||||
error_msg: Final = (
|
||||
f"Error in tracking cost callback - {e}\n Traceback:{traceback.format_exc()}\n"
|
||||
f" Args to _PROXY_track_cost_callback\n model: {failing_model}\n call_type: {failing_call_type}\n"
|
||||
)
|
||||
failing_litellm_params: Final = kwargs.get("litellm_params") or {}
|
||||
verbose_proxy_logger.debug(
|
||||
"Cost tracking callback failed for model=%s call_type=%s;"
|
||||
" chosen_metadata keys=%s litellm_metadata keys=%s old_metadata keys=%s",
|
||||
failing_model,
|
||||
failing_call_type,
|
||||
_metadata_keys(get_litellm_metadata_from_kwargs(kwargs=kwargs)),
|
||||
_metadata_keys(failing_litellm_params.get("litellm_metadata")),
|
||||
_metadata_keys(failing_litellm_params.get("metadata")),
|
||||
)
|
||||
asyncio.create_task(
|
||||
proxy_logging_obj.failed_tracking_alert(
|
||||
error_message=error_msg,
|
||||
failing_model=model,
|
||||
failing_model=failing_model,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -614,6 +623,12 @@ def _should_track_cost_callback(
|
|||
return call_type in _UNATTRIBUTED_TRACKABLE_CALL_TYPES
|
||||
|
||||
|
||||
def _metadata_keys(metadata: object) -> tuple[str, ...]:
|
||||
if not isinstance(metadata, Mapping):
|
||||
return ()
|
||||
return tuple(sorted(str(key) for key in metadata))
|
||||
|
||||
|
||||
def _get_budget_reservation_from_metadata(metadata: dict) -> dict | None:
|
||||
metadata_budget_reservation: Final = metadata.get("user_api_key_budget_reservation")
|
||||
if isinstance(metadata_budget_reservation, dict):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Final
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
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
|
||||
|
|
@ -2540,3 +2542,79 @@ async def test_async_post_call_failure_hook_persists_no_raw_model_on_an_unknown_
|
|||
== "/chat/completions: Invalid model name passed in. Call `/v1/models` to view available models for your key."
|
||||
)
|
||||
assert error_information["error_class"] == "ProxyModelNotFoundError"
|
||||
|
||||
|
||||
class _NeverStringifiedMetadataValue:
|
||||
def __repr__(self) -> str:
|
||||
raise AssertionError("a request metadata value was stringified by the cost tracking failure path")
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
def _spend_write_kwargs_with_metadata_value(metadata_value: object) -> dict:
|
||||
return {
|
||||
"call_type": "acompletion",
|
||||
"model": "gpt-5.4-mini",
|
||||
"litellm_call_id": "test-call-id",
|
||||
"stream": False,
|
||||
"response_cost": 4.725e-05,
|
||||
"litellm_params": {
|
||||
"metadata": {
|
||||
"user_api_key": "hashed-key",
|
||||
"user_api_key_user_id": "user-1",
|
||||
"user_context": metadata_value,
|
||||
"headers": {"user-agent": metadata_value},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("log_level", [logging.WARNING, logging.DEBUG])
|
||||
async def test_track_cost_callback_failure_alert_never_carries_request_metadata_values(log_level):
|
||||
logger: Final = _ProxyDBLogger()
|
||||
records: list[logging.LogRecord] = []
|
||||
handler: Final = logging.Handler()
|
||||
handler.emit = records.append
|
||||
previous_level: Final = verbose_proxy_logger.level
|
||||
verbose_proxy_logger.setLevel(log_level)
|
||||
verbose_proxy_logger.addHandler(handler)
|
||||
try:
|
||||
with patch( # test-quality-ok: callback imports proxy_logging_obj off proxy_server in its body, no seam
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj"
|
||||
) as mock_proxy_logging:
|
||||
mock_proxy_logging.failed_tracking_alert = AsyncMock()
|
||||
mock_proxy_logging.db_spend_update_writer = MagicMock()
|
||||
mock_proxy_logging.db_spend_update_writer.update_database = AsyncMock(
|
||||
side_effect=Exception("READONLY You can't write against a read only replica.")
|
||||
)
|
||||
|
||||
await logger._PROXY_track_cost_callback(
|
||||
kwargs=_spend_write_kwargs_with_metadata_value(_NeverStringifiedMetadataValue()),
|
||||
completion_response=ModelResponse(),
|
||||
start_time=datetime.now(),
|
||||
end_time=datetime.now(),
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
finally:
|
||||
verbose_proxy_logger.removeHandler(handler)
|
||||
verbose_proxy_logger.setLevel(previous_level)
|
||||
|
||||
mock_proxy_logging.failed_tracking_alert.assert_awaited_once()
|
||||
alert: Final = mock_proxy_logging.failed_tracking_alert.await_args.kwargs
|
||||
assert alert["failing_model"] == "gpt-5.4-mini"
|
||||
assert "READONLY You can't write against a read only replica." in alert["error_message"]
|
||||
assert "model: gpt-5.4-mini" in alert["error_message"]
|
||||
assert "call_type: acompletion" in alert["error_message"]
|
||||
|
||||
failure_debug_lines: Final = [
|
||||
record.getMessage()
|
||||
for record in records
|
||||
if record.levelno == logging.DEBUG and "Cost tracking callback failed" in record.getMessage()
|
||||
]
|
||||
if log_level == logging.DEBUG:
|
||||
assert len(failure_debug_lines) == 1
|
||||
assert "user_context" in failure_debug_lines[0]
|
||||
assert "headers" in failure_debug_lines[0]
|
||||
else:
|
||||
assert failure_debug_lines == []
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue