fix(auth): apply temp_budget_increase for cache-hit keys (#33841)

temp_budget_increase was only applied on the DB-fetch path of _user_api_key_auth_builder, so a key served from the auth cache reverted to its original max_budget and was wrongly blocked with BudgetExceededError once spend crossed the original budget while staying under the effective budget.

Move _update_key_budget_with_temp_budget_increase out of the DB-only branch so it runs for every resolved token regardless of source. The cache stores the original budget and each cache hit returns a fresh model_copy(), so this never double-applies.

Fixes #25760

Co-authored-by: shivam <shivam@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 089de50d20)
This commit is contained in:
devin-ai-integration[bot] 2026-07-20 18:12:15 -07:00 committed by Yuneng Jiang
parent 0f3fdbd072
commit e6ef379bb7
No known key found for this signature in database
2 changed files with 73 additions and 4 deletions

View file

@ -1670,10 +1670,9 @@ async def _user_api_key_auth_builder(
valid_token.end_user_tpm_limit = end_user_params.get("end_user_tpm_limit")
valid_token.end_user_rpm_limit = end_user_params.get("end_user_rpm_limit")
valid_token.allowed_model_region = end_user_params.get("allowed_model_region")
# update key budget with temp budget increase
valid_token = _update_key_budget_with_temp_budget_increase(
valid_token
) # updating it here, allows all downstream reporting / checks to use the updated budget
if valid_token is not None:
valid_token = _update_key_budget_with_temp_budget_increase(valid_token)
user_obj: Optional[LiteLLM_UserTable] = None
valid_token_dict: dict = {}

View file

@ -4441,3 +4441,73 @@ class TestCheckKeyModelBudgetWithFallback:
assert exc_info.value is original_error
assert "model" not in request_data
@pytest.mark.asyncio
async def test_temp_budget_increase_applied_for_cached_key():
"""
Regression for https://github.com/BerriAI/litellm/issues/25760
temp_budget_increase used to be applied only on the DB-fetch path, so a key
served from cache kept its original max_budget and was wrongly blocked once
spend crossed the original budget (but stayed under the effective budget).
Seed the auth cache with a key whose spend (5.0) exceeds its original
max_budget (2.0) but is under the effective budget (2.0 + 100.0). The cache-hit
request must not raise and the resolved token must carry max_budget == 102.0.
"""
from datetime import datetime, timedelta
from litellm.proxy.utils import hash_token
api_key = "sk-temp-budget-cache-regression"
hashed_token = hash_token(api_key)
expiry = (datetime.now() + timedelta(days=1)).isoformat()
cached_key = UserAPIKeyAuth(
token=hashed_token,
max_budget=2.0,
spend=5.0,
metadata={"temp_budget_increase": 100.0, "temp_budget_expiry": expiry},
)
user_api_key_cache = DualCache()
await _cache_key_object(
hashed_token=hashed_token,
user_api_key_obj=cached_key,
user_api_key_cache=user_api_key_cache,
proxy_logging_obj=None,
)
mock_request = MagicMock()
mock_request.url.path = "/v1/chat/completions"
mock_request.method = "POST"
mock_request.headers = {"authorization": f"Bearer {api_key}"}
mock_request.query_params = {}
mock_request.state = SimpleNamespace()
proxy_logging_obj = MagicMock()
proxy_logging_obj.budget_alerts = AsyncMock()
with (
patch("litellm.proxy.proxy_server.general_settings", {}),
patch("litellm.proxy.proxy_server.master_key", "sk-master"),
patch("litellm.proxy.proxy_server.prisma_client", MagicMock()),
patch("litellm.proxy.proxy_server.user_api_key_cache", user_api_key_cache),
patch("litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging_obj),
patch(
"litellm.proxy.auth.user_api_key_auth._virtual_key_max_budget_alert_check",
new_callable=AsyncMock,
),
):
result = await _user_api_key_auth_builder(
request=mock_request,
api_key=f"Bearer {api_key}",
azure_api_key_header="",
anthropic_api_key_header=None,
google_ai_studio_api_key_header=None,
azure_apim_header=None,
request_data={"model": "gpt-4o-mini"},
)
assert result.max_budget == 102.0