Merge pull request #41879 from BerriAI/litellm_jev_test_budget_1789764884

fix(proxy): enforce virtual key budgets for JEV test routing
This commit is contained in:
moe-berri 2026-09-20 10:10:21 -07:00 • committed by GitHub
commit 1e161f516c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 5 deletions

View file

@ -319,7 +319,7 @@ async def _authorize_models_this_test_can_call(
its calls through the proxy. Team and member budgets are already enforced on every route.
"""
models: Final = _models_this_test_can_call(config)
if not models:
if not models and config.classifier_type != "jev":
return
from litellm.proxy.proxy_server import proxy_logging_obj
@ -345,6 +345,14 @@ async def _authorize_models_this_test_can_call(
code=status.HTTP_400_BAD_REQUEST,
) from e
if config.classifier_type == "jev" and user_api_key_dict.budget_throttle_pct is not None:
raise ProxyException(
message="Budget has been exceeded! JEV Test Routing requires available budget.",
type=ProxyErrorTypes.budget_exceeded,
param=None,
code=status.HTTP_400_BAD_REQUEST,
)
@router.post(
"/auto_router/validate_complexity_router_config",

View file

@ -3,23 +3,34 @@ Unit tests for auto router management endpoints
"""
from collections.abc import Mapping, Sequence
from functools import partial
from pathlib import Path
from typing import Final
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import HTTPException, Request
from pydantic import ValidationError
import litellm
from litellm.proxy import proxy_server
from litellm.proxy._types import (
LitellmUserRoles,
ProxyErrorTypes,
ProxyException,
UserAPIKeyAuth,
)
from litellm.proxy.management_endpoints import auto_router_endpoints
from litellm.proxy.management_endpoints.auto_router_endpoints import (
preview_auto_router_routing,
)
from litellm.router import Router
from litellm.router_strategy.complexity_router import ComplexityRouter
from litellm.router_strategy.complexity_router.jev_classifier import (
JevChoiceAnswer,
JevClassifierClient,
JevSystemOneResponse,
)
from litellm.types.management_endpoints.auto_router_endpoints import (
AutoRouterBenchmarksResponse,
AutoRouterRoutingTestRequest,
@ -422,8 +433,115 @@ async def test_a_key_over_its_budget_cannot_run_a_classifier_config(monkeypatch:
assert calls == []
@pytest.mark.parametrize(
"max_budget, spend, denied",
(
pytest.param(0.0, 0.0, True, id="zero-budget"),
pytest.param(1.0, 1.0, True, id="budget-reached"),
pytest.param(1.0, 2.0, True, id="budget-exceeded"),
pytest.param(1.0, 0.5, False, id="budget-remaining"),
pytest.param(None, 2.0, False, id="unlimited"),
),
)
@pytest.mark.asyncio
async def test_a_heuristic_config_does_not_need_a_budget(monkeypatch: pytest.MonkeyPatch):
async def test_jev_test_routing_enforces_key_budget_before_provider_invocation(
monkeypatch: pytest.MonkeyPatch, max_budget: float | None, spend: float, denied: bool
) -> None:
client: Final = AsyncMock(spec=JevClassifierClient)
client.evaluate.return_value = JevSystemOneResponse(
model="jev-test",
answers={
"tier": JevChoiceAnswer(type="choice", choice="SIMPLE", probabilities={"SIMPLE": 1.0}, confidence=1.0)
},
)
monkeypatch.setattr(proxy_server, "llm_router", _router())
monkeypatch.setattr(auto_router_endpoints, "ComplexityRouter", partial(ComplexityRouter, jev_client=client))
actor: Final = UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN,
api_key="sk-jev-budget-test",
user_id="admin",
models=["cheap-model", "typesafe/jev-test"],
max_budget=max_budget,
spend=spend,
)
request: Final = _request(
"what is 2+2",
classifier_type="jev",
jev_classifier_config={"model": "jev-test"},
)
if denied:
with pytest.raises(ProxyException) as exc_info:
await preview_auto_router_routing(http_request=ROUTING_HTTP_REQUEST, data=request, user_api_key_dict=actor)
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
assert exc_info.value.code == "400"
assert exc_info.value.param is None
assert "Budget has been exceeded!" in exc_info.value.message
client.evaluate.assert_not_called()
return
response: Final = await preview_auto_router_routing(
http_request=ROUTING_HTTP_REQUEST, data=request, user_api_key_dict=actor
)
assert response.routed_model == "cheap-model"
assert response.routing_decision["cause"] == "jev_classifier"
assert response.routing_decision["classifier_model"] == "typesafe/jev-test"
client.evaluate.assert_awaited_once()
@pytest.mark.parametrize(
"max_budget, spend, denied",
((0.0, 0.0, True), (1.0, 2.0, True), (1.0, 0.5, False), (None, 2.0, False)),
)
@pytest.mark.asyncio
async def test_jev_test_routing_hard_blocks_exhausted_throttle_enabled_keys(
monkeypatch: pytest.MonkeyPatch, max_budget: float | None, spend: float, denied: bool
) -> None:
client: Final = AsyncMock(spec=JevClassifierClient)
client.evaluate.return_value = JevSystemOneResponse(
model="jev-test",
answers={
"tier": JevChoiceAnswer(type="choice", choice="SIMPLE", probabilities={"SIMPLE": 1.0}, confidence=1.0)
},
)
monkeypatch.setattr(litellm, "budget_exceeded_throttle_percentage", 0.1)
monkeypatch.setattr(proxy_server, "llm_router", _router())
monkeypatch.setattr(auto_router_endpoints, "ComplexityRouter", partial(ComplexityRouter, jev_client=client))
actor: Final = UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN,
api_key="sk-jev-throttle-test",
user_id="admin",
models=["cheap-model", "typesafe/jev-test"],
max_budget=max_budget,
spend=spend,
rpm_limit=100,
metadata={"throttle_on_budget_exceeded": True},
)
request: Final = _request(
"what is 2+2",
classifier_type="jev",
jev_classifier_config={"model": "jev-test"},
)
if denied:
with pytest.raises(ProxyException) as exc_info:
await preview_auto_router_routing(http_request=ROUTING_HTTP_REQUEST, data=request, user_api_key_dict=actor)
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
assert exc_info.value.code == "400"
client.evaluate.assert_not_called()
return
response: Final = await preview_auto_router_routing(
http_request=ROUTING_HTTP_REQUEST, data=request, user_api_key_dict=actor
)
assert response.routing_decision["cause"] == "jev_classifier"
client.evaluate.assert_awaited_once()
@pytest.mark.parametrize("max_budget, spend", ((0.0, 0.0), (1.0, 2.0)))
@pytest.mark.asyncio
async def test_a_heuristic_config_does_not_need_a_budget(
monkeypatch: pytest.MonkeyPatch, max_budget: float, spend: float
):
import litellm.proxy.proxy_server as proxy_server
monkeypatch.setattr(proxy_server, "llm_router", _router())
@ -435,8 +553,8 @@ async def test_a_heuristic_config_does_not_need_a_budget(monkeypatch: pytest.Mon
user_role=LitellmUserRoles.PROXY_ADMIN,
api_key="sk-broke",
user_id="admin",
max_budget=1.0,
spend=2.0,
max_budget=max_budget,
spend=spend,
models=["cheap-model"],
),
)
@ -877,7 +995,6 @@ class TestAutoRouterBenchmarks:
# ---------------------------------------------------------------------------
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, MagicMock
from litellm.proxy.management_endpoints.auto_router_endpoints import (
get_shadow_eval_job,