Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion litellm/proxy/management_endpoints/auto_router_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
veria-ai[bot] marked this conversation as resolved.
return

from litellm.proxy.proxy_server import proxy_logging_obj
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,34 @@
"""

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,
Expand Down Expand Up @@ -422,8 +433,115 @@ async def fail_if_called(**kwargs):
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_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):
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())
Expand All @@ -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"],
),
)
Expand Down Expand Up @@ -851,7 +969,6 @@ def test_the_listed_kinds_match_the_router_types_traffic_can_record(self):
# ---------------------------------------------------------------------------

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,
Expand Down
Loading