mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
* feat(router): add auto_router/quality_router for quality-tier routing (#25987) * feat(router): add auto_router/quality_router for quality-tier routing Adds a new auto-router type that routes a request to a model at a target quality tier. The quality tier is inferred by re-using the existing ComplexityRouter's classification, then mapped through an admin-configured complexity_to_quality table. Each candidate model declares its own quality_tier in model_info.litellm_routing_preferences. Resolution strategy: exact tier match, else round up to the next higher tier, else fall back to default_model. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * feat(quality_router): add capability-based filtering Each deployment can declare a `capabilities: List[str]` field in `model_info.litellm_routing_preferences` (e.g. ["vision", "function_calling"]). Requests can pass `litellm_capabilities` in `request_kwargs` to require specific capabilities — the router will only route to deployments whose declared capabilities are a superset. Resolution still walks tier (exact → round up), but at each tier filters by capability before picking. Falls back to default_model only when it also satisfies the required capabilities; otherwise raises rather than silently routing to a model that lacks a required capability. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * feat(quality_router): expose routing decision in response headers For transparency, expose the QualityRouter's routing decision in the proxy response headers: x-litellm-quality-router-model → picked model_name (e.g. "haiku-vision") x-litellm-quality-router-tier → resolved quality tier (e.g. "1") x-litellm-quality-router-complexity → ComplexityTier name (e.g. "SIMPLE") Mechanism: the pre-routing hook stashes the decision in request_kwargs["metadata"]["quality_router_decision"]. After the call returns, Router.set_response_headers lifts the decision into response._hidden_params["additional_headers"] alongside the existing x-litellm-model-group / x-litellm-model-id headers. Existing metadata keys (trace_id, user_id, etc.) are preserved. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * feat(quality_router): replace capabilities with keyword override Drops the capability-based filtering in favor of a keyword-based override for v0: - RoutingPreferences.keywords: List[str] (replaces capabilities) — each deployment can declare substring keywords. - If any declared keyword (case-insensitive) appears in the user message, the router short-circuits the complexity-classification flow and routes to the matching deployment. - Tiebreaker for overlapping keyword matches: quality_tier DESC, then cheapest model_info.input_cost_per_token ASC. Unpriced models lose ties to priced ones. Decision metadata + headers now expose the override: x-litellm-quality-router-via → "keyword" | "quality_tier" x-litellm-quality-router-keyword → matched keyword (only on keyword route) x-litellm-quality-router-complexity → complexity tier (only on tier route) Removes: - request_kwargs["litellm_capabilities"] reading - _model_capabilities, _model_supports_capabilities, _first_capable_model_at_tier, capability filter in _resolve_model_for_quality_tier Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * feat(quality_router): add explicit `order` to RoutingPreferences Adds an explicit priority field to RoutingPreferences for resolving collisions deterministically: RoutingPreferences.order: Optional[int] # lower wins; unset = +inf Used as the PRIMARY tiebreaker in two places: 1. Keyword overlap: when multiple deployments declare the same matching keyword, sort by (order ASC, quality_tier DESC, input_cost_per_token ASC, model_name ASC). Explicit always beats implicit. 2. Tier resolution: when multiple deployments share a quality tier, `_resolve_model_for_quality_tier` picks the one with the lowest order. The tier list is now sorted at index-build time. This lets admins make routing decisions explicit when the natural quality-and-price ordering would pick the wrong model. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * feat(quality_router): reorder tiebreak to (quality, order, price) Changes the tiebreak ordering so quality_tier always wins first, then explicit `order` is used to break ties within the same tier, then price breaks the rest: 1. quality_tier DESC ← best model wins first 2. order ASC ← explicit priority within a tier 3. input_cost_per_token ASC 4. model_name ASC Previously `order` was the primary key — that meant a tier-2 model with `order=1` would beat a tier-3 model with no `order`, which is the wrong default. Now `order` only resolves collisions among same-tier candidates. Tier resolution (within a single tier) keeps the same key minus quality: (order ASC, cost ASC, name). Test renames + flips: - test_explicit_order_overrides_quality_tier → test_quality_wins_over_explicit_order - new: test_order_breaks_tie_within_same_quality_tier Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * fix(quality_router): resolve Greptile review feedback Addresses four P1 findings from PR review plus test coverage: 1. set_model_list missing quality_routers reset - Hot-reloading the Router would leave stale QualityRouter instances pointing at the old model_list. `set_model_list` now clears `self.quality_routers` alongside the other indices. 2. Round-down fallback before default_model - `_resolve_model_for_quality_tier` now rounds DOWN to the closest lower tier after round-up fails, before falling back to `default_model`. Degrades gracefully rather than jumping straight off-tier. 3. RoutingPreferences validation bypass - `_build_tier_index` now instantiates `RoutingPreferences(**prefs)` so invalid shapes (e.g. non-int quality_tier) raise a clear ValueError instead of silently succeeding. 4. Config-ordering dependency - `_tier_to_models` is now built lazily on first access. Previously, eager construction in `__init__` meant a QualityRouter deployment had to appear AFTER all its referenced models in config.yaml, because `Router._create_deployment` populates `model_list` incrementally. Any `available_models` defined after the router entry would silently be reported as missing. Also adds 6 new tests covering each fix: - test_invalid_quality_tier_type_raises_clear_error - test_router_can_be_instantiated_before_its_targets_exist - test_set_model_list_clears_quality_routers_registry - test_rounds_down_when_no_higher_tier_exists - test_rounds_down_prefers_closest_lower_tier - test_prefers_round_up_over_round_down Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * style: apply black 24.10.0 formatting to pre-existing offenders Unblocks the LiteLLM Linting check for this PR — these 12 files are already failing `black --check` on main (the lint workflow only runs on PRs, so main drifts). No behavior changes; formatting-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update litellm/router.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Support /v1/responses in complexity router (#26137) * feat(proxy): add --reload flag for uvicorn hot reload (dev only) Opt-in CLI flag, off by default, no env var. Only affects the uvicorn run path; gunicorn/hypercorn paths and prod (which doesn't pass the flag) are unaffected. * Feature/add audio support for scaleway (#26110) * feat(scaleway): add SCALEWAY to LlmProviders enum * feat(scaleway): add audio transcription config and dispatch wiring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(scaleway): add behavior tests for audio transcription config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(scaleway): advertise audio_transcriptions in endpoint-support JSON * docs(scaleway): document audio transcription support * fix(scaleway): address PR review — plain-text response_format + missing-key fail-fast Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(scaleway): cover new response paths, drop gettysburg.wav coupling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * Prompt Compression - add it to the proxy (#25729) * refactor: new agentic loop event hook simplifies how to create logic for tool based multi llm calls * fix: compress - make it work on anthropic input as well * fix(compress.py): working prompt compression for claude code ensures claude code messages can run through proxy easily * docs: add agentic loop hook guide * docs: add agentic_loop_hook to sidebar * fix: fix multiple arguments error * fix: fix tool call loop for compression on streaming /v1/messages * fix: fix linting errors * fix: fix ci/cd errors * feat(litellm_pre_call_utils.py): use claude code session for litellm session id allows claude code logs to be stitched together, making it easy to know they were all part of the same conversation * fix: suppress incorrect mypy warning rE: module * revert: drop PR's changes to litellm/proxy/_experimental/out/ Restores the 34 HTML files under _experimental/out/ to their pre-PR paths (X/index.html -> X.html). All renames are R100 (content unchanged); no other files are touched. * fix: address greptile review comments on PR #25729 - Skip ``kwargs["tools"] = []`` injection when compression is a no-op — Anthropic Messages rejects empty tool arrays on requests that did not originally declare tools. - Move agentic-loop safety guards (fingerprint cycle / max depth) out of the per-callback try/except so they propagate instead of being swallowed by the generic exception handler. Extracted _check_agentic_loop_safety. - Gate generic ``x-<vendor>-session-id`` capture behind the LITELLM_CAPTURE_VENDOR_SESSION_HEADERS env var (off by default) to preserve backwards compatibility; explicit x-litellm-* headers are unaffected. - Fix monkeypatch target in pre-call-hook test to patch the actual module-level binding (litellm.integrations.compression_interception.handler.compress). - Add regression tests for empty-tools skip and opt-in session capture. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * revert: drop LITELLM_CAPTURE_VENDOR_SESSION_HEADERS flag Generic x-<vendor>-session-id header capture is a new feature and only runs *after* the explicit x-litellm-trace-id / x-litellm-session-id checks, so it does not change behavior for any existing caller that was already using the LiteLLM headers — no backwards-incompatibility to gate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(compress): replace input_type with CallTypes call_type Drop the bespoke ``CompressionInputType`` literal and use the existing ``litellm.types.utils.CallTypes`` enum instead. ``litellm.compress()`` now takes ``call_type: Union[CallTypes, str]`` (default ``CallTypes.completion``) — no new concept to learn, and the enum is already the way the rest of the codebase talks about request shapes. Supported values: ``completion`` / ``acompletion`` (OpenAI chat-completions shape) and ``anthropic_messages`` (Anthropic structured content blocks). Updated: compress(), the compression_interception handler, tests, docs, and the two eval scripts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Support /v1/responses in complexity router Adds cross-format support to the complexity router via the guardrail translation handler dispatch. Adds get_structured_messages to base translation plus OpenAI chat, Responses, and Anthropic handlers. Auto-router helper _extract_text_from_messages handles tool-call and multimodal messages. Widens async_pre_routing_hook messages type to Dict[str, Any]. Fixes https://github.com/BerriAI/litellm/issues/25134 * chore: apply black formatting * fix: fallback to trying each handler when route inference fails --------- Co-authored-by: Ryan Crabbe <ryan@berri.ai> Co-authored-by: nhyy244 <106547304+nhyy244@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * test: cover _is_quality_router_deployment and init_quality_router_deployment * fix: reset auto_routers on set_model_list to prevent hot-reload ValueError * style: apply black formatting to websearch_interception and agentic_streaming_iterator --------- Co-authored-by: yuneng-jiang <yuneng@berri.ai> Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Ryan Crabbe <ryan@berri.ai> Co-authored-by: nhyy244 <106547304+nhyy244@users.noreply.github.com>
1033 lines
36 KiB
Python
1033 lines
36 KiB
Python
"""
|
|
Tests for the QualityRouter.
|
|
|
|
Covers:
|
|
- Tier index construction from `model_info.litellm_routing_preferences`.
|
|
- Quality-tier resolution (exact, round-up, default fallback).
|
|
- Keyword override (match, tiebreaking by quality + price).
|
|
- Pre-routing hook end-to-end.
|
|
- Decision metadata stash + Router.set_response_headers lift.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from typing import Any, Dict, List
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.abspath("../../.."))
|
|
|
|
from litellm.router_strategy.quality_router.config import (
|
|
DEFAULT_COMPLEXITY_TO_QUALITY,
|
|
)
|
|
from litellm.router_strategy.quality_router.quality_router import QualityRouter
|
|
|
|
|
|
def _make_model_list(spec: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Build a router model_list from a compact spec.
|
|
|
|
spec entry shape: {
|
|
"model_name": str,
|
|
"quality_tier": Optional[int],
|
|
"keywords": Optional[List[str]],
|
|
"order": Optional[int],
|
|
"input_cost_per_token": Optional[float],
|
|
}
|
|
If quality_tier is None, the deployment is created without
|
|
`litellm_routing_preferences`.
|
|
"""
|
|
out: List[Dict[str, Any]] = []
|
|
for entry in spec:
|
|
model_info: Dict[str, Any] = {"id": f"id-{entry['model_name']}"}
|
|
if entry.get("quality_tier") is not None:
|
|
prefs: Dict[str, Any] = {"quality_tier": entry["quality_tier"]}
|
|
if "keywords" in entry:
|
|
prefs["keywords"] = entry["keywords"]
|
|
if "order" in entry:
|
|
prefs["order"] = entry["order"]
|
|
model_info["litellm_routing_preferences"] = prefs
|
|
if "input_cost_per_token" in entry:
|
|
model_info["input_cost_per_token"] = entry["input_cost_per_token"]
|
|
out.append(
|
|
{
|
|
"model_name": entry["model_name"],
|
|
"litellm_params": {"model": f"openai/{entry['model_name']}"},
|
|
"model_info": model_info,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
@pytest.fixture
|
|
def four_tier_model_list() -> List[Dict[str, Any]]:
|
|
"""A standard haiku(1)/sonnet(2)/opus(3)/opus-next(4) model list."""
|
|
return _make_model_list(
|
|
[
|
|
{"model_name": "haiku", "quality_tier": 1},
|
|
{"model_name": "sonnet", "quality_tier": 2},
|
|
{"model_name": "opus", "quality_tier": 3},
|
|
{"model_name": "opus-next", "quality_tier": 4},
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_router(four_tier_model_list):
|
|
"""A MagicMock router preloaded with the four-tier model list."""
|
|
router = MagicMock()
|
|
router.model_list = four_tier_model_list
|
|
return router
|
|
|
|
|
|
@pytest.fixture
|
|
def quality_router(mock_router) -> QualityRouter:
|
|
"""Default QualityRouter wired to all four tiers."""
|
|
config = {
|
|
"available_models": ["haiku", "sonnet", "opus", "opus-next"],
|
|
"complexity_to_quality": DEFAULT_COMPLEXITY_TO_QUALITY,
|
|
}
|
|
return QualityRouter(
|
|
model_name="quality-router-test",
|
|
litellm_router_instance=mock_router,
|
|
default_model="haiku",
|
|
quality_router_config=config,
|
|
)
|
|
|
|
|
|
# ─── Tier index ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTierIndex:
|
|
def test_builds_correct_tier_to_models_map(self, quality_router):
|
|
assert quality_router._tier_to_models == {
|
|
1: ["haiku"],
|
|
2: ["sonnet"],
|
|
3: ["opus"],
|
|
4: ["opus-next"],
|
|
}
|
|
|
|
def test_ignores_models_not_in_available_models(self, four_tier_model_list):
|
|
# Add a model the config doesn't list — it should be ignored.
|
|
extra = _make_model_list([{"model_name": "ghost", "quality_tier": 5}])
|
|
router = MagicMock()
|
|
router.model_list = four_tier_model_list + extra
|
|
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="haiku",
|
|
quality_router_config={
|
|
"available_models": ["haiku", "sonnet", "opus", "opus-next"]
|
|
},
|
|
)
|
|
|
|
for models in qr._tier_to_models.values():
|
|
assert "ghost" not in models
|
|
|
|
def test_raises_when_routing_preferences_missing(self):
|
|
# `sonnet` is in available_models but has no preferences.
|
|
ml = _make_model_list(
|
|
[
|
|
{"model_name": "haiku", "quality_tier": 1},
|
|
{"model_name": "sonnet", "quality_tier": None},
|
|
]
|
|
)
|
|
router = MagicMock()
|
|
router.model_list = ml
|
|
|
|
# Construction succeeds (tier index is lazy); the error surfaces on
|
|
# first use so the router entry doesn't have to appear after all of
|
|
# its referenced models in config.yaml.
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="haiku",
|
|
quality_router_config={"available_models": ["haiku", "sonnet"]},
|
|
)
|
|
with pytest.raises(ValueError, match="sonnet"):
|
|
_ = qr._tier_to_models
|
|
|
|
|
|
# ─── Resolve model for quality tier ─────────────────────────────────────────
|
|
|
|
|
|
class TestResolveModelForQualityTier:
|
|
def test_exact_match(self, quality_router):
|
|
assert quality_router._resolve_model_for_quality_tier(2) == "sonnet"
|
|
assert quality_router._resolve_model_for_quality_tier(4) == "opus-next"
|
|
|
|
def test_rounds_up_when_tier_missing(self, mock_router):
|
|
# Available tiers: 1, 3, 4. Asking for 2 should round up to 3.
|
|
spec = [
|
|
{"model_name": "haiku", "quality_tier": 1},
|
|
{"model_name": "opus", "quality_tier": 3},
|
|
{"model_name": "opus-next", "quality_tier": 4},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="haiku",
|
|
quality_router_config={"available_models": ["haiku", "opus", "opus-next"]},
|
|
)
|
|
|
|
assert qr._resolve_model_for_quality_tier(2) == "opus"
|
|
|
|
def test_rounds_down_when_no_higher_tier_exists(self):
|
|
# Only tier 1 available. Asking for tier 4 rounds up (nothing), then
|
|
# rounds DOWN to the closest lower tier — tier 1.
|
|
spec = [{"model_name": "haiku", "quality_tier": 1}]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="emergency-default",
|
|
quality_router_config={"available_models": ["haiku"]},
|
|
)
|
|
|
|
assert qr._resolve_model_for_quality_tier(4) == "haiku"
|
|
|
|
def test_rounds_down_prefers_closest_lower_tier(self):
|
|
# Available: 1, 2. Asking for 4 rounds down to tier 2 (not tier 1).
|
|
spec = [
|
|
{"model_name": "haiku", "quality_tier": 1},
|
|
{"model_name": "sonnet", "quality_tier": 2},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="emergency-default",
|
|
quality_router_config={"available_models": ["haiku", "sonnet"]},
|
|
)
|
|
|
|
assert qr._resolve_model_for_quality_tier(4) == "sonnet"
|
|
|
|
def test_prefers_round_up_over_round_down(self):
|
|
# Available: 1, 3. Asking for 2 rounds UP to 3, not DOWN to 1.
|
|
spec = [
|
|
{"model_name": "haiku", "quality_tier": 1},
|
|
{"model_name": "opus", "quality_tier": 3},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="emergency-default",
|
|
quality_router_config={"available_models": ["haiku", "opus"]},
|
|
)
|
|
|
|
assert qr._resolve_model_for_quality_tier(2) == "opus"
|
|
|
|
|
|
# ─── RoutingPreferences validation ─────────────────────────────────────────
|
|
|
|
|
|
class TestRoutingPreferencesValidation:
|
|
def test_invalid_quality_tier_type_raises_clear_error(self):
|
|
# quality_tier must be an int — pass a non-coercible string.
|
|
ml = [
|
|
{
|
|
"model_name": "haiku",
|
|
"litellm_params": {"model": "openai/gpt-4o-mini"},
|
|
"model_info": {
|
|
"id": "id-haiku",
|
|
"litellm_routing_preferences": {"quality_tier": "not-an-int"},
|
|
},
|
|
}
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = ml
|
|
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="haiku",
|
|
quality_router_config={"available_models": ["haiku"]},
|
|
)
|
|
with pytest.raises(ValueError, match="invalid litellm_routing_preferences"):
|
|
_ = qr._tier_to_models
|
|
|
|
|
|
# ─── Config-ordering independence (lazy index build) ───────────────────────
|
|
|
|
|
|
class TestConfigOrderingIndependence:
|
|
def test_router_can_be_instantiated_before_its_targets_exist(self):
|
|
# Build a router instance whose referenced model_list is EMPTY at
|
|
# construction time (simulating a config where the router entry
|
|
# appears before its target deployments). The tier index must not be
|
|
# built eagerly — it's deferred until first use.
|
|
router = MagicMock()
|
|
router.model_list = [] # <- targets haven't been added yet
|
|
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="haiku",
|
|
quality_router_config={"available_models": ["haiku", "sonnet", "opus"]},
|
|
)
|
|
|
|
# Now the targets come online. This mirrors the incremental add by
|
|
# `Router._create_deployment`.
|
|
router.model_list = _make_model_list(
|
|
[
|
|
{"model_name": "haiku", "quality_tier": 1},
|
|
{"model_name": "sonnet", "quality_tier": 2},
|
|
{"model_name": "opus", "quality_tier": 3},
|
|
]
|
|
)
|
|
|
|
# First access triggers the index build and sees the full list.
|
|
assert qr._tier_to_models == {
|
|
1: ["haiku"],
|
|
2: ["sonnet"],
|
|
3: ["opus"],
|
|
}
|
|
|
|
|
|
# ─── Router.set_model_list resets quality_routers (hot reload) ─────────────
|
|
|
|
|
|
class TestSetModelListResetsQualityRouters:
|
|
def test_set_model_list_clears_quality_routers_registry(self):
|
|
from litellm.router import Router
|
|
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "haiku",
|
|
"litellm_params": {
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_key": "sk-test",
|
|
},
|
|
"model_info": {"litellm_routing_preferences": {"quality_tier": 1}},
|
|
},
|
|
{
|
|
"model_name": "my-qr",
|
|
"litellm_params": {
|
|
"model": "auto_router/quality_router",
|
|
"quality_router_default_model": "haiku",
|
|
"quality_router_config": {"available_models": ["haiku"]},
|
|
},
|
|
},
|
|
]
|
|
)
|
|
|
|
assert "my-qr" in router.quality_routers
|
|
|
|
# Hot-reload with a new model_list that doesn't define the router.
|
|
router.set_model_list(
|
|
[
|
|
{
|
|
"model_name": "haiku",
|
|
"litellm_params": {
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_key": "sk-test",
|
|
},
|
|
}
|
|
]
|
|
)
|
|
|
|
# Stale router from before must be cleared.
|
|
assert "my-qr" not in router.quality_routers
|
|
|
|
|
|
# ─── Pre-routing hook ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPreRoutingHook:
|
|
@pytest.mark.asyncio
|
|
async def test_simple_message_routes_to_tier_1(self, quality_router):
|
|
messages = [{"role": "user", "content": "hi"}]
|
|
resp = await quality_router.async_pre_routing_hook(
|
|
model="quality-router-test",
|
|
request_kwargs={},
|
|
messages=messages,
|
|
)
|
|
assert resp is not None
|
|
assert resp.model == "haiku"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reasoning_message_routes_to_tier_4(self, quality_router):
|
|
# Two reasoning markers triggers ComplexityTier.REASONING → quality 4.
|
|
messages = [
|
|
{
|
|
"role": "user",
|
|
"content": (
|
|
"Think step by step and reason through this problem. "
|
|
"Analyze this carefully and break down each component."
|
|
),
|
|
}
|
|
]
|
|
resp = await quality_router.async_pre_routing_hook(
|
|
model="quality-router-test",
|
|
request_kwargs={},
|
|
messages=messages,
|
|
)
|
|
assert resp is not None
|
|
assert resp.model == "opus-next"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_messages_returns_none(self, quality_router):
|
|
resp = await quality_router.async_pre_routing_hook(
|
|
model="quality-router-test",
|
|
request_kwargs={},
|
|
messages=[],
|
|
)
|
|
assert resp is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_only_system_message_routes_to_default(self, quality_router):
|
|
messages = [{"role": "system", "content": "You are a helpful assistant."}]
|
|
resp = await quality_router.async_pre_routing_hook(
|
|
model="quality-router-test",
|
|
request_kwargs={},
|
|
messages=messages,
|
|
)
|
|
assert resp is not None
|
|
assert resp.model == "haiku" # the configured default_model
|
|
|
|
|
|
# ─── Keyword override ──────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture
|
|
def keyword_router():
|
|
"""
|
|
Router where multiple deployments declare overlapping keywords so we can
|
|
exercise the (quality DESC, price ASC) tiebreak.
|
|
|
|
- cheap-coder tier 2, keywords [code, python], cost 0.000001
|
|
- smart-coder tier 3, keywords [code, python], cost 0.000010
|
|
- law-bot tier 2, keywords [legal, contract], cost 0.000005
|
|
- default-haiku tier 1, no keywords, cost 0.0000005
|
|
"""
|
|
spec = [
|
|
{
|
|
"model_name": "default-haiku",
|
|
"quality_tier": 1,
|
|
"keywords": [],
|
|
"input_cost_per_token": 0.0000005,
|
|
},
|
|
{
|
|
"model_name": "cheap-coder",
|
|
"quality_tier": 2,
|
|
"keywords": ["code", "python"],
|
|
"input_cost_per_token": 0.000001,
|
|
},
|
|
{
|
|
"model_name": "smart-coder",
|
|
"quality_tier": 3,
|
|
"keywords": ["code", "python"],
|
|
"input_cost_per_token": 0.000010,
|
|
},
|
|
{
|
|
"model_name": "law-bot",
|
|
"quality_tier": 2,
|
|
"keywords": ["legal", "contract"],
|
|
"input_cost_per_token": 0.000005,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
return QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="default-haiku",
|
|
quality_router_config={
|
|
"available_models": [
|
|
"default-haiku",
|
|
"cheap-coder",
|
|
"smart-coder",
|
|
"law-bot",
|
|
],
|
|
},
|
|
)
|
|
|
|
|
|
class TestKeywordOverride:
|
|
def test_no_keyword_in_message_returns_none(self, keyword_router):
|
|
assert keyword_router._keyword_override("hello there") is None
|
|
|
|
def test_single_match_returns_that_model(self, keyword_router):
|
|
# Only law-bot declares "legal".
|
|
assert keyword_router._keyword_override("review this legal doc") == (
|
|
"law-bot",
|
|
"legal",
|
|
)
|
|
|
|
def test_case_insensitive_match(self, keyword_router):
|
|
assert keyword_router._keyword_override("LEGAL question") == (
|
|
"law-bot",
|
|
"legal",
|
|
)
|
|
|
|
def test_overlap_picks_highest_quality_tier(self, keyword_router):
|
|
# Both cheap-coder (tier 2) and smart-coder (tier 3) declare "code".
|
|
# Quality wins over price → smart-coder.
|
|
assert keyword_router._keyword_override("write some code for me") == (
|
|
"smart-coder",
|
|
"code",
|
|
)
|
|
|
|
def test_same_tier_picks_cheapest(self):
|
|
# Two models at the same tier, both matching "data" — cheapest wins.
|
|
spec = [
|
|
{
|
|
"model_name": "expensive",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
"input_cost_per_token": 0.000050,
|
|
},
|
|
{
|
|
"model_name": "cheap",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
"input_cost_per_token": 0.000005,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="cheap",
|
|
quality_router_config={"available_models": ["expensive", "cheap"]},
|
|
)
|
|
match = qr._keyword_override("show me the data")
|
|
assert match == ("cheap", "data")
|
|
|
|
def test_unpriced_loses_to_priced_at_same_tier(self):
|
|
# Same quality tier, one has cost, one doesn't → priced wins.
|
|
spec = [
|
|
{
|
|
"model_name": "no-price",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
# input_cost_per_token deliberately omitted
|
|
},
|
|
{
|
|
"model_name": "with-price",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
"input_cost_per_token": 0.000005,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="no-price",
|
|
quality_router_config={"available_models": ["no-price", "with-price"]},
|
|
)
|
|
match = qr._keyword_override("show me the data")
|
|
assert match == ("with-price", "data")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hook_short_circuits_complexity_on_keyword_match(
|
|
self, keyword_router
|
|
):
|
|
# A reasoning-style prompt would normally route to a high-quality model
|
|
# via the complexity flow — but the keyword "code" should short-circuit
|
|
# to smart-coder (highest tier among "code" models).
|
|
messages = [
|
|
{
|
|
"role": "user",
|
|
"content": (
|
|
"Think step by step and reason through this code problem. "
|
|
"Analyze this carefully and break down each component."
|
|
),
|
|
}
|
|
]
|
|
request_kwargs: Dict[str, Any] = {}
|
|
resp = await keyword_router.async_pre_routing_hook(
|
|
model="qr",
|
|
request_kwargs=request_kwargs,
|
|
messages=messages,
|
|
)
|
|
assert resp is not None
|
|
assert resp.model == "smart-coder"
|
|
|
|
decision = request_kwargs["metadata"]["quality_router_decision"]
|
|
assert decision["routed_via"] == "keyword"
|
|
assert decision["matched_keyword"] == "code"
|
|
assert decision["complexity_tier"] is None # short-circuited
|
|
|
|
def test_quality_wins_over_explicit_order(self):
|
|
# Quality always beats order. A tier-3 model with no `order` wins over
|
|
# a tier-2 model with `order=1`.
|
|
spec = [
|
|
{
|
|
"model_name": "ordered-tier2",
|
|
"quality_tier": 2,
|
|
"keywords": ["code"],
|
|
"order": 1,
|
|
"input_cost_per_token": 0.000010,
|
|
},
|
|
{
|
|
"model_name": "implicit-tier3",
|
|
"quality_tier": 3,
|
|
"keywords": ["code"],
|
|
"input_cost_per_token": 0.000005,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="ordered-tier2",
|
|
quality_router_config={
|
|
"available_models": ["ordered-tier2", "implicit-tier3"]
|
|
},
|
|
)
|
|
match = qr._keyword_override("write some code")
|
|
assert match == ("implicit-tier3", "code")
|
|
|
|
def test_order_breaks_tie_within_same_quality_tier(self):
|
|
# Two tier-3 models, both match "code". Lower `order` wins.
|
|
spec = [
|
|
{
|
|
"model_name": "preferred",
|
|
"quality_tier": 3,
|
|
"keywords": ["code"],
|
|
"order": 1,
|
|
"input_cost_per_token": 0.000050, # more expensive
|
|
},
|
|
{
|
|
"model_name": "default-tier3",
|
|
"quality_tier": 3,
|
|
"keywords": ["code"],
|
|
"input_cost_per_token": 0.000005, # cheaper
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="default-tier3",
|
|
quality_router_config={"available_models": ["preferred", "default-tier3"]},
|
|
)
|
|
match = qr._keyword_override("write some code")
|
|
assert match == ("preferred", "code")
|
|
|
|
def test_explicit_order_overrides_price(self):
|
|
# Same tier, but the more expensive one has a lower `order` and wins.
|
|
spec = [
|
|
{
|
|
"model_name": "expensive-but-preferred",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
"order": 1,
|
|
"input_cost_per_token": 0.000050,
|
|
},
|
|
{
|
|
"model_name": "cheap-default",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
"input_cost_per_token": 0.000005,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="cheap-default",
|
|
quality_router_config={
|
|
"available_models": ["expensive-but-preferred", "cheap-default"]
|
|
},
|
|
)
|
|
match = qr._keyword_override("show me the data")
|
|
assert match == ("expensive-but-preferred", "data")
|
|
|
|
def test_lower_order_wins_between_two_explicitly_ordered(self):
|
|
spec = [
|
|
{
|
|
"model_name": "second",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
"order": 5,
|
|
},
|
|
{
|
|
"model_name": "first",
|
|
"quality_tier": 2,
|
|
"keywords": ["data"],
|
|
"order": 1,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="first",
|
|
quality_router_config={"available_models": ["first", "second"]},
|
|
)
|
|
match = qr._keyword_override("show me the data")
|
|
assert match == ("first", "data")
|
|
|
|
def test_same_order_falls_through_to_quality_then_price(self):
|
|
# All three models share order=1 → tiebreak falls through to
|
|
# (quality DESC, cost ASC).
|
|
spec = [
|
|
{
|
|
"model_name": "low-tier",
|
|
"quality_tier": 1,
|
|
"keywords": ["data"],
|
|
"order": 1,
|
|
"input_cost_per_token": 0.000001,
|
|
},
|
|
{
|
|
"model_name": "high-tier-cheap",
|
|
"quality_tier": 3,
|
|
"keywords": ["data"],
|
|
"order": 1,
|
|
"input_cost_per_token": 0.000005,
|
|
},
|
|
{
|
|
"model_name": "high-tier-expensive",
|
|
"quality_tier": 3,
|
|
"keywords": ["data"],
|
|
"order": 1,
|
|
"input_cost_per_token": 0.000050,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="low-tier",
|
|
quality_router_config={
|
|
"available_models": [
|
|
"low-tier",
|
|
"high-tier-cheap",
|
|
"high-tier-expensive",
|
|
]
|
|
},
|
|
)
|
|
match = qr._keyword_override("show me the data")
|
|
assert match == ("high-tier-cheap", "data")
|
|
|
|
def test_order_is_used_in_tier_resolution_too(self):
|
|
# Two models at the same tier. Explicit `order=1` on the second one
|
|
# should make _resolve_model_for_quality_tier(2) pick it.
|
|
spec = [
|
|
{
|
|
"model_name": "default-pick",
|
|
"quality_tier": 2,
|
|
},
|
|
{
|
|
"model_name": "preferred-pick",
|
|
"quality_tier": 2,
|
|
"order": 1,
|
|
},
|
|
]
|
|
router = MagicMock()
|
|
router.model_list = _make_model_list(spec)
|
|
qr = QualityRouter(
|
|
model_name="qr",
|
|
litellm_router_instance=router,
|
|
default_model="default-pick",
|
|
quality_router_config={
|
|
"available_models": ["default-pick", "preferred-pick"]
|
|
},
|
|
)
|
|
assert qr._resolve_model_for_quality_tier(2) == "preferred-pick"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hook_falls_back_to_complexity_when_no_keyword(self, keyword_router):
|
|
# No declared keyword in the message → complexity-based routing.
|
|
# "hi" is SIMPLE → quality 1 → default-haiku (the only tier-1 model).
|
|
messages = [{"role": "user", "content": "hi"}]
|
|
request_kwargs: Dict[str, Any] = {}
|
|
resp = await keyword_router.async_pre_routing_hook(
|
|
model="qr",
|
|
request_kwargs=request_kwargs,
|
|
messages=messages,
|
|
)
|
|
assert resp is not None
|
|
assert resp.model == "default-haiku"
|
|
|
|
decision = request_kwargs["metadata"]["quality_router_decision"]
|
|
assert decision["routed_via"] == "quality_tier"
|
|
assert decision["matched_keyword"] is None
|
|
assert decision["complexity_tier"] == "SIMPLE"
|
|
|
|
|
|
# ─── Routing-decision metadata (powers x-litellm-quality-router-* headers) ──
|
|
|
|
|
|
class TestDecisionMetadata:
|
|
@pytest.mark.asyncio
|
|
async def test_hook_stashes_decision_in_request_kwargs_metadata(
|
|
self, quality_router
|
|
):
|
|
# Reasoning prompt → REASONING → quality tier 4 → opus-next.
|
|
messages = [
|
|
{
|
|
"role": "user",
|
|
"content": (
|
|
"Think step by step and reason through this problem. "
|
|
"Analyze this carefully and break down each component."
|
|
),
|
|
}
|
|
]
|
|
request_kwargs: Dict[str, Any] = {}
|
|
|
|
resp = await quality_router.async_pre_routing_hook(
|
|
model="quality-router-test",
|
|
request_kwargs=request_kwargs,
|
|
messages=messages,
|
|
)
|
|
assert resp is not None and resp.model == "opus-next"
|
|
|
|
decision = request_kwargs["metadata"]["quality_router_decision"]
|
|
assert decision["routed_model"] == "opus-next"
|
|
assert decision["quality_tier"] == 4
|
|
assert decision["complexity_tier"] == "REASONING"
|
|
assert decision["router_model_name"] == "quality-router-test"
|
|
assert decision["routed_via"] == "quality_tier"
|
|
assert decision["matched_keyword"] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_decision_metadata_preserves_existing_metadata(self, quality_router):
|
|
request_kwargs: Dict[str, Any] = {
|
|
"metadata": {"trace_id": "abc-123", "user_id": "u-1"}
|
|
}
|
|
|
|
await quality_router.async_pre_routing_hook(
|
|
model="quality-router-test",
|
|
request_kwargs=request_kwargs,
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
)
|
|
|
|
# Existing metadata keys are intact and the decision is added alongside.
|
|
assert request_kwargs["metadata"]["trace_id"] == "abc-123"
|
|
assert request_kwargs["metadata"]["user_id"] == "u-1"
|
|
assert "quality_router_decision" in request_kwargs["metadata"]
|
|
|
|
|
|
# ─── Router.set_response_headers lifts decision into x-litellm-quality-* ────
|
|
|
|
|
|
class TestSetResponseHeadersLiftsDecision:
|
|
"""
|
|
Verify the Router.set_response_headers helper turns a stashed quality-router
|
|
decision into x-litellm-quality-router-* headers on the response.
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifts_decision_into_additional_headers(self):
|
|
from pydantic import BaseModel
|
|
|
|
from litellm.router import Router
|
|
|
|
class FakeResponse(BaseModel):
|
|
model_config = {"arbitrary_types_allowed": True}
|
|
_hidden_params: Dict[str, Any] = {}
|
|
|
|
# Build a real Router with a tiny model_list — enough to satisfy
|
|
# set_response_headers without needing the rest of the router stack.
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "haiku",
|
|
"litellm_params": {
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_key": "sk-test",
|
|
},
|
|
}
|
|
]
|
|
)
|
|
|
|
response = FakeResponse()
|
|
response._hidden_params = {}
|
|
|
|
request_kwargs = {
|
|
"metadata": {
|
|
"quality_router_decision": {
|
|
"router_model_name": "qr",
|
|
"routed_model": "smart-coder",
|
|
"routed_via": "keyword",
|
|
"matched_keyword": "code",
|
|
"quality_tier": 3,
|
|
"complexity_tier": None,
|
|
}
|
|
}
|
|
}
|
|
|
|
await router.set_response_headers(
|
|
response=response,
|
|
model_group="qr",
|
|
request_kwargs=request_kwargs,
|
|
)
|
|
|
|
headers = response._hidden_params["additional_headers"]
|
|
assert headers["x-litellm-quality-router-model"] == "smart-coder"
|
|
assert headers["x-litellm-quality-router-tier"] == "3"
|
|
assert headers["x-litellm-quality-router-via"] == "keyword"
|
|
assert headers["x-litellm-quality-router-keyword"] == "code"
|
|
# Keyword route short-circuits classification → no complexity header.
|
|
assert "x-litellm-quality-router-complexity" not in headers
|
|
# Existing x-litellm-model-group behavior is unchanged.
|
|
assert headers["x-litellm-model-group"] == "qr"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quality_tier_route_emits_complexity_not_keyword(self):
|
|
from pydantic import BaseModel
|
|
|
|
from litellm.router import Router
|
|
|
|
class FakeResponse(BaseModel):
|
|
model_config = {"arbitrary_types_allowed": True}
|
|
_hidden_params: Dict[str, Any] = {}
|
|
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "haiku",
|
|
"litellm_params": {
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_key": "sk-test",
|
|
},
|
|
}
|
|
]
|
|
)
|
|
|
|
response = FakeResponse()
|
|
response._hidden_params = {}
|
|
|
|
request_kwargs = {
|
|
"metadata": {
|
|
"quality_router_decision": {
|
|
"router_model_name": "qr",
|
|
"routed_model": "haiku",
|
|
"routed_via": "quality_tier",
|
|
"matched_keyword": None,
|
|
"quality_tier": 1,
|
|
"complexity_tier": "SIMPLE",
|
|
}
|
|
}
|
|
}
|
|
|
|
await router.set_response_headers(
|
|
response=response,
|
|
model_group="qr",
|
|
request_kwargs=request_kwargs,
|
|
)
|
|
|
|
headers = response._hidden_params["additional_headers"]
|
|
assert headers["x-litellm-quality-router-via"] == "quality_tier"
|
|
assert headers["x-litellm-quality-router-complexity"] == "SIMPLE"
|
|
# Quality-tier route → no keyword header.
|
|
assert "x-litellm-quality-router-keyword" not in headers
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_decision_leaves_quality_router_headers_unset(self):
|
|
from pydantic import BaseModel
|
|
|
|
from litellm.router import Router
|
|
|
|
class FakeResponse(BaseModel):
|
|
model_config = {"arbitrary_types_allowed": True}
|
|
_hidden_params: Dict[str, Any] = {}
|
|
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "haiku",
|
|
"litellm_params": {
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_key": "sk-test",
|
|
},
|
|
}
|
|
]
|
|
)
|
|
|
|
response = FakeResponse()
|
|
response._hidden_params = {}
|
|
|
|
await router.set_response_headers(
|
|
response=response,
|
|
model_group="haiku",
|
|
request_kwargs={}, # no quality_router_decision
|
|
)
|
|
|
|
headers = response._hidden_params["additional_headers"]
|
|
assert "x-litellm-quality-router-model" not in headers
|
|
assert "x-litellm-quality-router-tier" not in headers
|
|
|
|
|
|
class TestRouterQualityDeploymentMethods:
|
|
"""Tests for Router._is_quality_router_deployment and Router.init_quality_router_deployment."""
|
|
|
|
def test_is_quality_router_deployment_true(self):
|
|
"""_is_quality_router_deployment returns True for quality router models."""
|
|
from litellm.router import Router
|
|
from litellm.types.router import LiteLLM_Params
|
|
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "gpt-4o-mini",
|
|
"litellm_params": {"model": "openai/gpt-4o-mini"},
|
|
}
|
|
]
|
|
)
|
|
params = LiteLLM_Params(model="auto_router/quality_router/my-router")
|
|
assert router._is_quality_router_deployment(params) is True
|
|
|
|
def test_is_quality_router_deployment_false(self):
|
|
"""_is_quality_router_deployment returns False for regular models."""
|
|
from litellm.router import Router
|
|
from litellm.types.router import LiteLLM_Params
|
|
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "gpt-4o-mini",
|
|
"litellm_params": {"model": "openai/gpt-4o-mini"},
|
|
}
|
|
]
|
|
)
|
|
params = LiteLLM_Params(model="openai/gpt-4o-mini")
|
|
assert router._is_quality_router_deployment(params) is False
|
|
|
|
def test_init_quality_router_deployment(self):
|
|
"""init_quality_router_deployment registers a QualityRouter."""
|
|
from litellm.router import Router
|
|
from litellm.types.router import Deployment, LiteLLM_Params
|
|
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "gpt-4o-mini",
|
|
"litellm_params": {"model": "openai/gpt-4o-mini"},
|
|
}
|
|
]
|
|
)
|
|
deployment = Deployment(
|
|
model_name="auto_router/quality_router/test-router",
|
|
litellm_params=LiteLLM_Params(
|
|
model="auto_router/quality_router/test-router",
|
|
quality_router_default_model="gpt-4o-mini",
|
|
),
|
|
model_info={"id": "test-id"},
|
|
)
|
|
router.init_quality_router_deployment(deployment)
|
|
assert "auto_router/quality_router/test-router" in router.quality_routers
|