Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_helpers/auto_router_availability.py: 91%
45 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1from collections.abc import Mapping, Sequence
2from copy import deepcopy
3from dataclasses import dataclass
4from types import MappingProxyType
5from typing import Final
7from pydantic import BaseModel, Json, TypeAdapter, ValidationError
9from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
10from litellm.router_utils.auto_router_model_naming import (
11 GATED_AUTO_ROUTER_CAPABILITIES,
12 capability_limit_violation,
13 classify_strategy_router_model,
14 count_capability_routers,
15 gated_capability_of,
16)
17from litellm.router_utils.auto_router_tuning_baseline import (
18 is_mutable_tuned_candidate,
19 mutable_tuned_identities,
20 tuning_quota_violation,
21)
22from litellm.types.management_endpoints.auto_router_endpoints import (
23 AutoRouterAllowance,
24 AutoRouterAvailabilityResponse,
25)
28class _CatalogModelInfo(BaseModel):
29 team_id: str | None = None
32class _CatalogSource(BaseModel):
33 model_id: str
34 created_by: str | None = None
35 litellm_params: Json[dict[str, object]] | dict[str, object]
36 model_info: Json[_CatalogModelInfo] | _CatalogModelInfo | None = None
39@dataclass(frozen=True, slots=True)
40class AutoRouterCatalogEntry:
41 model_id: str
42 team_id: str | None
43 created_by: str | None
44 deployment: Mapping[str, object]
47def _catalog_field(value: object, key: str) -> object:
48 if not isinstance(value, str): 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 return deepcopy(value)
50 return decrypt_value_helper(value, key=key, exception_type="debug", return_original_value=True)
53def build_auto_router_catalog(rows: Sequence[object]) -> tuple[AutoRouterCatalogEntry, ...] | None:
54 try:
55 sources: Final = TypeAdapter(tuple[_CatalogSource, ...]).validate_python(rows, from_attributes=True)
56 except ValidationError:
57 return None
58 return tuple(
59 AutoRouterCatalogEntry(
60 model_id=row.model_id,
61 team_id=row.model_info.team_id if row.model_info is not None else None,
62 created_by=row.created_by,
63 deployment=MappingProxyType(
64 {
65 "litellm_params": MappingProxyType(
66 {
67 "model": model,
68 "complexity_router_config": _catalog_field(
69 row.litellm_params.get("complexity_router_config"), "complexity_router_config"
70 ),
71 }
72 ),
73 "model_info": MappingProxyType({"id": row.model_id, "db_model": True}),
74 }
75 ),
76 )
77 for row in sources
78 if isinstance(model := _catalog_field(row.litellm_params.get("model"), "model"), str)
79 and classify_strategy_router_model(model) == "complexity"
80 )
83def auto_router_availability(
84 *,
85 others: Sequence[Mapping[str, object]],
86 existing: Mapping[str, object] | None,
87 candidate: Mapping[str, object],
88 baselines: Mapping[str, str] | None,
89 limit: int | None,
90) -> AutoRouterAvailabilityResponse:
91 existing_params: Final = None if existing is None else existing.get("litellm_params")
92 candidate_params: Final = candidate.get("litellm_params")
93 owned: Final = gated_capability_of(existing_params) if isinstance(existing_params, Mapping) else None
94 claimed: Final = gated_capability_of(candidate_params) if isinstance(candidate_params, Mapping) else None
95 counts: Final = tuple(
96 (capability, count_capability_routers(others, capability=capability))
97 for capability in GATED_AUTO_ROUTER_CAPABILITIES
98 )
99 tuned_count: Final = len(mutable_tuned_identities(others, baselines)) if baselines is not None else 0
100 allowances: Final = tuple(
101 AutoRouterAllowance(
102 key=capability.key,
103 limit=limit,
104 remaining=None if limit is None else max(0, limit - held),
105 used_by_this_router=owned is capability,
106 )
107 for capability, held in counts
108 )
109 capability_error: Final = next(
110 (
111 capability_limit_violation(capability=capability, held=held + 1, limit=limit)
112 for capability, held in counts
113 if capability is claimed
114 ),
115 None,
116 )
117 tuning_error: Final = (
118 tuning_quota_violation(candidate=candidate, others=others, baselines=baselines, limit=limit)
119 if baselines is not None
120 else None
121 )
122 capability_labels: Final = {
123 "heuristic_v2": "Heuristic v2",
124 "capability": "Capability",
125 "llm_v2": "Fuse v2",
126 "tier_or_classifier_prompt": "Custom tiers or classifier instructions",
127 }
128 return AutoRouterAvailabilityResponse(
129 allowances=(
130 *allowances,
131 AutoRouterAllowance(
132 key="heuristic_tuning",
133 limit=limit,
134 remaining=None if limit is None or baselines is None else max(0, limit - tuned_count),
135 available=limit is None or baselines is not None,
136 used_by_this_router=bool(
137 existing is not None and baselines is not None and is_mutable_tuned_candidate(existing, baselines)
138 ),
139 ),
140 ),
141 error=(
142 f"{capability_labels[claimed.key]} has no available allowance. Choose another option or free an existing allowance."
143 if capability_error is not None and claimed is not None
144 else "These scoring rules need an available Rule-based tuning allowance. Check the weights, thresholds, keywords, and custom dimensions in Advanced settings. Model choices do not use this allowance."
145 if tuning_error is not None
146 else None
147 ),
148 )