diff --git a/litellm/router.py b/litellm/router.py index 5d04839d5bf..9b1933ee7a9 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -7725,6 +7725,7 @@ class Router: default_model=default_model, litellm_router_instance=self, complexity_router_config=complexity_router_config, + savings_baseline_model=deployment.litellm_params.auto_router_savings_baseline_model, ) self._register_pre_routing_strategy( registry=self.complexity_routers, diff --git a/litellm/router_strategy/auto_router/auto_router.py b/litellm/router_strategy/auto_router/auto_router.py index cd074cd9097..2f0ae4e164f 100644 --- a/litellm/router_strategy/auto_router/auto_router.py +++ b/litellm/router_strategy/auto_router/auto_router.py @@ -55,113 +55,19 @@ class AutoRouter(CustomLogger): self.litellm_router_instance: "Router" = litellm_router_instance self.configured_savings_baseline_model: str | None = savings_baseline_model - @staticmethod - def _canonical_model(model: str, custom_llm_provider: str | None) -> str | None: - """``provider/model``, or ``None`` when the pair names no known provider. - - A deployment may name its vendor either in the model prefix or in a separate - `custom_llm_provider`, and the bare name alone is not enough to price: it can - resolve to a different vendor's rates, or to nothing at all. Qualifying it here - means the baseline that reaches the spend writer resolves back to the same - vendor that served it. - """ - import litellm - - try: - resolved_model, provider, _, _ = litellm.get_llm_provider( - model=model, custom_llm_provider=custom_llm_provider - ) - except Exception as e: # noqa: BLE001 # an unroutable candidate cannot be the baseline - verbose_router_logger.debug("auto-router savings: cannot resolve candidate %s (%s)", model, e) - return None - return f"{provider}/{resolved_model}" - - def _deployment_model(self, index: int) -> str | None: - """The model a deployment calls, qualified by the provider it declares.""" - params = self.litellm_router_instance.model_list[index].get("litellm_params") - if not isinstance(params, dict): - return None - model = params.get("model") - return self._canonical_model(model, params.get("custom_llm_provider")) if model else None - - def _models_for_group(self, group_name: str) -> tuple[str, ...]: - """The models a route's model group actually calls, or the name itself when the - parent router has no deployment under it.""" - indices = self.litellm_router_instance.model_name_to_deployment_indices.get(group_name) - if not indices: - canonical = self._canonical_model(group_name, None) - return (canonical,) if canonical else () - return tuple(model for index in indices if (model := self._deployment_model(index))) - - def _candidate_models(self) -> tuple[str, ...]: - """Every model this router can route to, as pricable model names. - - Routes name the router's own model groups rather than models, so each is - resolved through the parent router's deployments before anything is priced. - """ - group_names = frozenset( - name for name in (*(route.name for route in self.loaded_routes), self.default_model) if name - ) - return tuple(model for group_name in group_names for model in self._models_for_group(group_name)) - - @staticmethod - def _priced_candidate(model: str) -> tuple[float, float, str] | None: - """``(output_rate, input_rate, model)``, or ``None`` when the model has no pricing.""" - import litellm - - try: - info = litellm.get_model_info(model=model) - except Exception as e: # noqa: BLE001 # unmapped candidates simply cannot be the baseline - verbose_router_logger.debug("auto-router savings: no pricing for candidate %s (%s)", model, e) - return None - output_rate = info.get("output_cost_per_token") or 0.0 - input_rate = info.get("input_cost_per_token") or 0.0 - if output_rate <= 0.0 and input_rate <= 0.0: - # A model that costs nothing per token cannot stand in for what the traffic - # would otherwise have cost, and as a baseline it would report the whole - # real spend as a loss. - verbose_router_logger.debug("auto-router savings: candidate %s has no per-token price", model) - return None - return (output_rate, input_rate, model) - - def _most_expensive_candidate(self) -> str | None: - """The priciest candidate by output rate, input rate breaking the tie.""" - priced = tuple( - candidate for model in self._candidate_models() if (candidate := self._priced_candidate(model)) is not None - ) - if not priced: - verbose_router_logger.debug("auto-router savings: no priceable candidates; savings driver disabled") - return None - return max(priced)[2] - @property def savings_baseline_model(self) -> str | None: """The model this router's savings are measured against. - Without the router a deployment has to pick one model, and it has to be one that - can carry the hardest request, so the counterfactual is the priciest model this - router could have chosen. Deriving it from the router's own candidates keeps it - honest: a fixed flagship credits savings against a model the operator would - never have run, and drifts the moment the routes change. - - Always provider-qualified, whether derived or configured, because it travels to - the spend writer as a bare string with no provider beside it; an operator who - writes `deepseek-r1` meaning Azure would otherwise be priced against whoever - owns that name. - - Derived per call rather than cached: the parent router adds and removes - deployments while it runs, so a baseline pinned on first use would keep naming a - model the router no longer has, and a pricier one added later could never become - the baseline. Resolving costs tens of microseconds against a network call, which - is not worth trading correctness for. - - ``None`` when nothing can be priced, which zeroes the driver rather than - inventing a baseline. + Every model group its routes can reach is a candidate, because any of them + could have been the one model a deployment picked without the router. """ - configured = self.configured_savings_baseline_model - if configured: - return self._canonical_model(configured, None) - return self._most_expensive_candidate() + from litellm.router_strategy.savings_baseline import resolve_baseline + + group_names = frozenset( + name for name in (*(route.name for route in self.loaded_routes), self.default_model) if name + ) + return resolve_baseline(self.configured_savings_baseline_model, self.litellm_router_instance, group_names) def _load_semantic_routing_routes(self) -> List[Route]: from semantic_router.routers import SemanticRouter diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index b43fe0da4ca..504cf7000d8 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -314,6 +314,7 @@ class ComplexityRouter(CustomLogger): litellm_router_instance: Router, complexity_router_config: dict[str, Any] | None = None, default_model: str | None = None, + savings_baseline_model: str | None = None, ): """ Initialize ComplexityRouter. @@ -323,9 +324,11 @@ class ComplexityRouter(CustomLogger): litellm_router_instance: The LiteLLM Router instance. complexity_router_config: Optional configuration dict from proxy config. default_model: Optional default model to use if tier cannot be determined. + savings_baseline_model: Overrides the counterfactual model the dashboard measures savings against; derived from the hardest configured tier when unset. """ self.model_name = model_name self.litellm_router_instance = litellm_router_instance + self.configured_savings_baseline_model: str | None = savings_baseline_model # Parse config - always create a new instance to avoid singleton mutation if complexity_router_config: @@ -373,6 +376,36 @@ class ComplexityRouter(CustomLogger): verbose_router_logger.debug(f"ComplexityRouter initialized for {model_name} with tiers: {self.config.tiers}") + def _hardest_tier_models(self) -> tuple[str, ...]: + """The model or pool serving the hardest tier this router configures. + + REASONING when it is configured, since that is the tier a request has to be + hard enough to reach; otherwise the highest-severity tier present, so a + deployment that only defines SIMPLE and MEDIUM is still measured against + the best it could actually have picked. + """ + for tier in reversed(TIER_SEVERITY_ORDER): + models = self.config.tiers.get(tier.value) + if models: + return tuple(models) if isinstance(models, list) else (models,) + return () + + @property + def savings_baseline_model(self) -> str | None: + """The model this router's savings are measured against. + + A complexity router's tier ladder already names the model an operator + would have had to run to serve the hardest request, so the counterfactual + is the priciest model in that tier rather than the priciest model the + router can reach; a cheap tier is a choice the router made, not a ceiling + it was bounded by. + """ + from litellm.router_strategy.savings_baseline import resolve_baseline + + return resolve_baseline( + self.configured_savings_baseline_model, self.litellm_router_instance, self._hardest_tier_models() + ) + def _estimate_tokens(self, text: str) -> int: """ Estimate token count from text. @@ -1367,6 +1400,7 @@ class ComplexityRouter(CustomLogger): return PreRoutingHookResponse( model=routed_model, messages=messages if has_original_messages else None, + savings_baseline_model=self.savings_baseline_model, routing_decision=self._build_routing_decision( routed_model=routed_model, cause=cause, @@ -1443,6 +1477,7 @@ class ComplexityRouter(CustomLogger): return PreRoutingHookResponse( model=routed_model, messages=messages if has_original_messages else None, + savings_baseline_model=self.savings_baseline_model, routing_decision=self._build_routing_decision(routed_model=routed_model, cause="default_fallback"), ) @@ -1464,6 +1499,7 @@ class ComplexityRouter(CustomLogger): return PreRoutingHookResponse( model=routed_model, messages=messages if has_original_messages else None, + savings_baseline_model=self.savings_baseline_model, routing_decision=self._build_routing_decision( routed_model=routed_model, cause=keyword_cause, @@ -1511,6 +1547,7 @@ class ComplexityRouter(CustomLogger): return PreRoutingHookResponse( model=routed_model, messages=messages if has_original_messages else None, + savings_baseline_model=self.savings_baseline_model, routing_decision=self._build_routing_decision( routed_model=routed_model, cause=outcome.cause, diff --git a/litellm/router_strategy/savings_baseline.py b/litellm/router_strategy/savings_baseline.py new file mode 100644 index 00000000000..eddccb70453 --- /dev/null +++ b/litellm/router_strategy/savings_baseline.py @@ -0,0 +1,127 @@ +"""Resolving the counterfactual model a strategy router's savings are measured against. + +Without the router a deployment has to pick one model, and it has to be one that +can carry the hardest request it will see. That model is the baseline: what the +traffic would have cost had nobody routed it. + +Every strategy router answers the same two questions differently, so the shared +part is here and the per-router part is the candidate set it supplies. A semantic +auto-router offers every model group its routes can reach; a complexity router +offers the models in its hardest tier. + +Baselines are always provider-qualified, whether derived or configured, because +they travel to the spend writer as a bare string with no provider beside them; an +operator who writes ``deepseek-r1`` meaning Azure would otherwise be priced +against whoever else owns that name. +""" + +from collections.abc import Iterable +from typing import TYPE_CHECKING + +from litellm._logging import verbose_router_logger + +if TYPE_CHECKING: + from litellm.router import Router + + +def canonical_model(model: str, custom_llm_provider: str | None = None) -> str | None: + """``provider/model``, or ``None`` when the pair names no known provider. + + A deployment may name its vendor in the model prefix or in a separate + ``custom_llm_provider``, and the bare name alone is not enough to price: it + can resolve to a different vendor's rates, or to nothing at all. + """ + import litellm + + try: + resolved_model, provider, _, _ = litellm.get_llm_provider(model=model, custom_llm_provider=custom_llm_provider) + except Exception as e: # noqa: BLE001 # an unroutable candidate cannot be the baseline + verbose_router_logger.debug("savings baseline: cannot resolve candidate %s (%s)", model, e) + return None + return f"{provider}/{resolved_model}" + + +def _deployment_model(router: "Router", index: int) -> str | None: + """The model a deployment is priced as, qualified by the provider it declares. + + `litellm_params.model` is not always a model. On Azure it is the deployment name, + which is absent from the cost map, and `model_info.base_model` is what names the + real model; the same holds for wildcard and aliased deployments. Router.py resolves + pricing through the same base_model, base_model, model chain. + """ + deployment = router.model_list[index] + params = deployment.get("litellm_params") + if not isinstance(params, dict): + return None + model_info = deployment.get("model_info") + base_model = model_info.get("base_model") if isinstance(model_info, dict) else None + model = base_model or params.get("base_model") or params.get("model") + return canonical_model(model, params.get("custom_llm_provider")) if model else None + + +def models_for_group(router: "Router", group_name: str) -> tuple[str, ...]: + """The models a model group actually calls. + + Falls back to treating the name as a model itself, which is what a tier + pointing straight at a provider model rather than at a configured group does. + """ + indices = router.model_name_to_deployment_indices.get(group_name) + if not indices: + canonical = canonical_model(group_name) + return (canonical,) if canonical else () + return tuple(model for index in indices if (model := _deployment_model(router, index))) + + +def _priced(model: str) -> tuple[float, float, str] | None: + """``(output_rate, input_rate, model)``, or ``None`` when the model has no pricing.""" + import litellm + + try: + info = litellm.get_model_info(model=model) + except Exception as e: # noqa: BLE001 # unmapped candidates simply cannot be the baseline + verbose_router_logger.debug("savings baseline: no pricing for candidate %s (%s)", model, e) + return None + output_rate = info.get("output_cost_per_token") or 0.0 + input_rate = info.get("input_cost_per_token") or 0.0 + if output_rate <= 0.0 and input_rate <= 0.0: + # A model that costs nothing per token cannot stand in for what the traffic + # would otherwise have cost, and as a baseline it would report the whole + # real spend as a loss. + verbose_router_logger.debug("savings baseline: candidate %s has no per-token price", model) + return None + return (output_rate, input_rate, model) + + +def most_expensive(models: Iterable[str]) -> str | None: + """The priciest model by output rate, input rate breaking the tie.""" + priced = tuple(candidate for model in models if (candidate := _priced(model)) is not None) + if not priced: + verbose_router_logger.debug("savings baseline: no priceable candidates; savings driver disabled") + return None + return max(priced)[2] + + +def resolve_baseline(configured: str | None, router: "Router", group_names: Iterable[str]) -> str | None: + """The baseline for a router offering ``group_names`` as its candidates. + + A configured override wins and is only qualified, never re-derived. Otherwise + the groups are resolved through the parent router's deployments and the + priciest result is taken. + + Derived per call rather than cached: the parent router adds and removes + deployments while it runs, so a baseline pinned on first use would keep naming + a model the router no longer has, and a pricier one added later could never + become the baseline. Resolving costs tens of microseconds against a network + call, which is not worth trading correctness for. + + Never raises. This is read on the routing path to decorate a request that is + about to be served, and a dashboard's counterfactual is not worth failing a + live request over; an unresolvable baseline zeroes the savings driver instead. + """ + try: + if configured: + return canonical_model(configured) + return most_expensive(model for group_name in group_names for model in models_for_group(router, group_name)) + except Exception as e: # noqa: BLE001 # see docstring: routing must not fail for a metric + verbose_router_logger.warning("savings baseline: could not resolve, savings will read zero (%s)", e) + return None diff --git a/tests/test_litellm/router_strategy/test_auto_router.py b/tests/test_litellm/router_strategy/test_auto_router.py index f4537e834cf..6e2f54ec499 100644 --- a/tests/test_litellm/router_strategy/test_auto_router.py +++ b/tests/test_litellm/router_strategy/test_auto_router.py @@ -422,10 +422,13 @@ class TestSavingsBaselineModel: ["cheap-tier", "mid-tier"], "cheap-tier", ) - assert sorted(auto_router._candidate_models()) == [ - "anthropic/claude-haiku-4-5", - "anthropic/claude-sonnet-5", - ] + from litellm.router_strategy.savings_baseline import models_for_group + + parent = auto_router.litellm_router_instance + resolved = sorted( + model for group in ("cheap-tier", "mid-tier") for model in models_for_group(parent, group) + ) + assert resolved == ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5"] def test_baseline_is_the_priciest_model_this_router_could_have_picked(self): """Without the router a deployment picks one model that can carry the hardest diff --git a/tests/test_litellm/router_strategy/test_complexity_router.py b/tests/test_litellm/router_strategy/test_complexity_router.py index c776e5bb072..1b2331135cc 100644 --- a/tests/test_litellm/router_strategy/test_complexity_router.py +++ b/tests/test_litellm/router_strategy/test_complexity_router.py @@ -2185,6 +2185,8 @@ class FakeEmbeddingRouter: _CLUSTER_MARKERS = ("k8s", "kube", "container", "cluster", "orchestrat") def __init__(self): + self.model_name_to_deployment_indices: Dict[str, List[int]] = {} + self.model_list: List[Dict] = [] self.async_embedding_calls: List[List[str]] = [] self.async_embedding_kwargs: List[Dict] = [] # Every embedded batch (sync route-index build AND async query), so tests can count @@ -4782,3 +4784,61 @@ class TestClassifierTrustBoundary: assert system_message["content"] == _CLASSIFICATION_SYSTEM_RUBRIC assert hostile not in system_message["content"] assert hostile in user_message["content"] + + +class TestSavingsBaselineModel: + """The counterfactual model a complexity router's savings are measured against.""" + + @staticmethod + def _router_with_tiers(tiers: dict, **kwargs) -> ComplexityRouter: + from litellm.router import Router + + parent = Router( + model_list=[ + {"model_name": "cheap", "litellm_params": {"model": "anthropic/claude-haiku-4-5"}}, + {"model_name": "mid", "litellm_params": {"model": "anthropic/claude-sonnet-4-5"}}, + {"model_name": "top", "litellm_params": {"model": "anthropic/claude-opus-4-5"}}, + ] + ) + return ComplexityRouter( + model_name="bench", + litellm_router_instance=parent, + complexity_router_config={"tiers": tiers}, + default_model="mid", + **kwargs, + ) + + def test_baseline_is_the_reasoning_tier_not_the_priciest_reachable_model(self): + """The ladder names what an operator would have had to run for the hardest + request; a pricier model sitting in a lower tier is a choice, not a ceiling.""" + router = self._router_with_tiers({"SIMPLE": "top", "MEDIUM": "cheap", "REASONING": "mid"}) + assert router.savings_baseline_model == "anthropic/claude-sonnet-4-5" + + def test_baseline_is_the_priciest_model_when_the_reasoning_tier_is_a_pool(self): + router = self._router_with_tiers({"SIMPLE": "cheap", "REASONING": ["cheap", "top", "mid"]}) + assert router.savings_baseline_model == "anthropic/claude-opus-4-5" + + def test_falls_back_to_the_hardest_configured_tier_when_reasoning_is_absent(self): + router = self._router_with_tiers({"SIMPLE": "cheap", "COMPLEX": "top"}) + assert router.savings_baseline_model == "anthropic/claude-opus-4-5" + + def test_a_configured_baseline_wins_and_is_provider_qualified(self): + router = self._router_with_tiers( + {"SIMPLE": "cheap", "REASONING": "mid"}, savings_baseline_model="claude-opus-4-5" + ) + assert router.savings_baseline_model == "anthropic/claude-opus-4-5" + + def test_an_unpriceable_tier_disables_the_driver_rather_than_inventing_a_baseline(self): + router = self._router_with_tiers({"REASONING": "not-a-real-model-anywhere"}) + assert router.savings_baseline_model is None + + def test_the_baseline_travels_on_every_pre_routing_response(self): + """A response without it silently zeroes the savings driver for that path.""" + import inspect + + from litellm.router_strategy.complexity_router import complexity_router as module + + source = inspect.getsource(module.ComplexityRouter.async_pre_routing_hook) + returns = source.count("return PreRoutingHookResponse(") + assert returns > 0 + assert source.count("savings_baseline_model=self.savings_baseline_model") == returns diff --git a/tests/test_litellm/router_strategy/test_savings_baseline.py b/tests/test_litellm/router_strategy/test_savings_baseline.py new file mode 100644 index 00000000000..4b98ed737a2 --- /dev/null +++ b/tests/test_litellm/router_strategy/test_savings_baseline.py @@ -0,0 +1,155 @@ +import pytest + +from litellm.router import Router +from litellm.router_strategy.savings_baseline import ( + canonical_model, + models_for_group, + most_expensive, + resolve_baseline, +) + + +@pytest.fixture +def parent() -> Router: + return Router( + model_list=[ + {"model_name": "cheap", "litellm_params": {"model": "anthropic/claude-haiku-4-5"}}, + {"model_name": "top", "litellm_params": {"model": "anthropic/claude-opus-4-5"}}, + {"model_name": "pool", "litellm_params": {"model": "anthropic/claude-haiku-4-5"}}, + {"model_name": "pool", "litellm_params": {"model": "anthropic/claude-opus-4-5"}}, + ] + ) + + +class TestCanonicalModel: + def test_qualifies_a_bare_name_with_the_provider_that_owns_it(self): + assert canonical_model("claude-opus-4-5") == "anthropic/claude-opus-4-5" + + def test_keeps_an_already_qualified_name_qualified(self): + assert canonical_model("anthropic/claude-opus-4-5") == "anthropic/claude-opus-4-5" + + def test_honours_a_separately_declared_provider(self): + assert canonical_model("claude-opus-4-5", "openai") == "openai/claude-opus-4-5" + + def test_returns_none_for_a_name_no_provider_claims(self): + assert canonical_model("") is None + + +class TestModelsForGroup: + def test_resolves_a_group_to_the_models_its_deployments_call(self, parent): + assert models_for_group(parent, "cheap") == ("anthropic/claude-haiku-4-5",) + + def test_returns_every_deployment_in_a_pooled_group(self, parent): + assert sorted(models_for_group(parent, "pool")) == [ + "anthropic/claude-haiku-4-5", + "anthropic/claude-opus-4-5", + ] + + def test_treats_an_unknown_group_as_a_model_name(self, parent): + """A tier can point straight at a provider model rather than a configured group.""" + assert models_for_group(parent, "claude-opus-4-5") == ("anthropic/claude-opus-4-5",) + + +class TestMostExpensive: + def test_picks_by_output_rate(self): + assert ( + most_expensive(["anthropic/claude-haiku-4-5", "anthropic/claude-opus-4-5"]) + == "anthropic/claude-opus-4-5" + ) + + def test_ignores_models_with_no_per_token_price(self): + """A free model as baseline would report the whole real spend as a loss.""" + assert most_expensive(["not-a-real-model-anywhere", "anthropic/claude-haiku-4-5"]) == ( + "anthropic/claude-haiku-4-5" + ) + + def test_returns_none_when_nothing_can_be_priced(self): + assert most_expensive(["not-a-real-model-anywhere"]) is None + + def test_returns_none_for_an_empty_candidate_set(self): + assert most_expensive([]) is None + + +class TestResolveBaseline: + def test_a_configured_baseline_wins_over_the_candidates(self, parent): + assert resolve_baseline("claude-haiku-4-5", parent, ["top"]) == "anthropic/claude-haiku-4-5" + + def test_derives_the_priciest_candidate_when_unconfigured(self, parent): + assert resolve_baseline(None, parent, ["cheap", "top"]) == "anthropic/claude-opus-4-5" + + def test_never_raises_so_a_metric_cannot_fail_a_live_request(self): + """Read on the routing path while decorating a request that is about to be + served; a dashboard counterfactual must not be able to take routing down.""" + + class Exploding: + @property + def model_name_to_deployment_indices(self): + raise RuntimeError("router is mid-reload") + + assert resolve_baseline(None, Exploding(), ["anything"]) is None + + def test_an_empty_candidate_set_zeroes_the_driver_rather_than_inventing_one(self, parent): + assert resolve_baseline(None, parent, []) is None + + +class TestDeploymentsPricedByBaseModel: + """`litellm_params.model` is not always a model. + + On Azure it is the deployment name, which is absent from the cost map, so pricing it + directly drops the candidate. If that candidate was the priciest, the baseline quietly + becomes the second priciest and every saving is understated; if the whole pool is + Azure, nothing prices and the driver reports zero with nothing at default log level + saying why. `model_info.base_model` is what names the real model, which is the chain + router.py already resolves pricing through. + """ + + @staticmethod + def _router(*deployments: dict) -> Router: + return Router(model_list=list(deployments)) + + def test_model_info_base_model_is_preferred_over_the_deployment_name(self): + router = self._router( + { + "model_name": "big", + "litellm_params": {"model": "azure/my-gpt5-deployment"}, + "model_info": {"base_model": "azure/gpt-4.1"}, + }, + ) + assert models_for_group(router, "big") == ("azure/gpt-4.1",) + + def test_litellm_params_base_model_is_the_other_accepted_spelling(self): + router = self._router( + { + "model_name": "big", + "litellm_params": {"model": "azure/my-gpt5-deployment", "base_model": "azure/gpt-4.1"}, + }, + ) + assert models_for_group(router, "big") == ("azure/gpt-4.1",) + + def test_a_deployment_without_a_base_model_still_prices_by_its_model(self): + router = self._router({"model_name": "big", "litellm_params": {"model": "anthropic/claude-opus-4-5"}}) + assert models_for_group(router, "big") == ("anthropic/claude-opus-4-5",) + + def test_an_azure_deployment_can_win_the_priciest_candidate(self): + """Without the base_model hop the Azure candidate never prices, so the cheaper + model wins by default and the reported saving shrinks.""" + router = self._router( + {"model_name": "cheap", "litellm_params": {"model": "anthropic/claude-haiku-4-5"}}, + { + "model_name": "big", + "litellm_params": {"model": "azure/my-gpt5-deployment"}, + "model_info": {"base_model": "azure/gpt-4.1"}, + }, + ) + assert resolve_baseline(None, router, ["cheap", "big"]) == "azure/gpt-4.1" + + def test_an_all_azure_pool_still_has_a_baseline(self): + """Otherwise nothing prices, the driver is disabled and the card reads $0.00.""" + router = self._router( + { + "model_name": "big", + "litellm_params": {"model": "azure/my-gpt5-deployment"}, + "model_info": {"base_model": "azure/gpt-4.1"}, + }, + ) + assert resolve_baseline(None, router, ["big"]) == "azure/gpt-4.1"