Merge pull request #39960 from BerriAI/litellm_cache_hit_guardrail_spend

fix(proxy): keep guardrail cost in spend on cache hits
This commit is contained in:
ryan-crabbe-berri 2026-09-05 15:53:34 -07:00 committed by GitHub
commit a0c9095cfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 5 deletions

View file

@ -254,7 +254,7 @@ class _ProxyDBLogger(CustomLogger):
key_alias: Final = cast(str | None, metadata.get("user_api_key_alias", None))
end_user_max_budget: Final = metadata.get("user_api_end_user_max_budget", None)
sl_object: Final[StandardLoggingPayload | None] = kwargs.get("standard_logging_object", None)
response_cost = (
response_cost: Final = (
sl_object.get("response_cost", None) if sl_object is not None else kwargs.get("response_cost", None)
)
tags: Final = _get_request_tags_for_cost_tracking(
@ -269,10 +269,6 @@ class _ProxyDBLogger(CustomLogger):
if response_cost is not None:
user_api_key: Final = metadata.get("user_api_key", None)
if kwargs.get("cache_hit", False) is True:
response_cost = 0.0
verbose_proxy_logger.debug("Cache Hit: response_cost %s, for user_id %s", response_cost, user_id)
verbose_proxy_logger.debug(
"user_api_key %s, user_id %s, team_id %s, end_user_id %s",
user_api_key,

View file

@ -1783,6 +1783,53 @@ async def test_track_cost_callback_enriches_user_id_for_mcp_style_metadata():
)
@pytest.mark.asyncio
async def test_track_cost_callback_keeps_guardrail_cost_on_cache_hit():
"""A cache hit skips the LLM, not the guardrail that screened the prompt, so the
guardrail's provider charge must still reach spend logs and budgets. The payload
already prices the LLM share at 0 on a cache hit, so its response_cost is the
guardrail cost alone and the callback must pass it through untouched."""
logger = _ProxyDBLogger()
kwargs = {
"call_type": "acompletion",
"model": "gpt-4o",
"cache_hit": True,
"response_cost": 0.0,
"litellm_params": {
"metadata": {
"user_api_key": "hashed-key",
"user_api_key_user_id": "user-1",
"user_api_key_team_id": "team-1",
}
},
"standard_logging_object": {
"response_cost": 0.0003,
"request_tags": [],
"metadata": {},
"cost_breakdown": {"guardrail_cost": 0.0003, "total_cost": 0.0003},
},
}
with (
patch("litellm.proxy.proxy_server.increment_spend_counters", new_callable=AsyncMock) as mock_increment, # test-quality-ok: the callback imports this from proxy_server inside its body, so there is no injection seam
patch("litellm.proxy.proxy_server.update_cache", new_callable=AsyncMock), # test-quality-ok: same function-body import, no injection seam
patch("litellm.proxy.proxy_server.proxy_logging_obj") as mock_proxy_logging, # test-quality-ok: same function-body import, no injection seam
):
mock_proxy_logging.db_spend_update_writer.update_database = AsyncMock()
mock_proxy_logging.slack_alerting_instance.customer_spend_alert = AsyncMock()
await logger._PROXY_track_cost_callback(
kwargs=kwargs,
completion_response={"id": "cached-call-1"},
start_time=datetime.now(),
end_time=datetime.now(),
)
update_kwargs = mock_proxy_logging.db_spend_update_writer.update_database.await_args.kwargs
assert update_kwargs["response_cost"] == pytest.approx(0.0003)
assert mock_increment.call_args.kwargs["response_cost"] == pytest.approx(0.0003)
@pytest.mark.parametrize(
"call_type, expected",
[