fix(spend): remove the proxy-wide autorouter savings baseline override (#38700)

Every complexity router now derives and records its savings baseline from
its hardest configured tier, and the spend writer always prices against the
decision-recorded baseline model and deployment id. A leftover
litellm_settings.autorouter_savings_baseline_model key is inert
This commit is contained in:
tin-berri 2026-08-28 14:53:27 -07:00 committed by GitHub
parent 3fa4a4b72a
commit fb80ba7c98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 28 additions and 54 deletions

View file

@ -274,7 +274,6 @@ databricks_key: Optional[str] = None
openai_like_key: Optional[str] = None
azure_key: Optional[str] = None
anthropic_key: Optional[str] = None
autorouter_savings_baseline_model: Optional[str] = None
replicate_key: Optional[str] = None
bytez_key: Optional[str] = None
gdc_key: Optional[str] = None

View file

@ -502,14 +502,11 @@ def autorouter_savings_for_request(
usage: Final = _usage_from_spend_log(usage_object)
if usage is None or not model:
return None
# The configured `autorouter_savings_baseline_model` wins; otherwise the baseline
# the deciding router recorded on its decision; neither means the driver is off.
decision: Final = routing_decision if isinstance(routing_decision, Mapping) else {}
recorded: Final = decision.get("savings_baseline_model")
recorded_id: Final = decision.get("savings_baseline_deployment_id")
configured: Final = litellm.autorouter_savings_baseline_model
baseline_model: Final = configured or (recorded if isinstance(recorded, str) else None)
baseline_id: Final = recorded_id if configured is None and isinstance(recorded_id, str) else None
baseline_model: Final = recorded if isinstance(recorded, str) else None
baseline_id: Final = recorded_id if isinstance(recorded_id, str) else None
if not decision or not baseline_model:
return None
router_instance: Final = llm_router() if llm_router else None

View file

@ -947,17 +947,15 @@ class ComplexityRouter(CustomLogger):
def savings_baseline(self) -> Baseline | None:
"""The derived counterfactual this router's savings are measured against.
``None`` when `litellm_settings.autorouter_savings_baseline_model` is set (the
spend writer reads that setting directly and it wins) or when this router was
built with ``derive_savings_baseline=False``. Derived once on first use and
pinned for the instance's lifetime: creating or editing the router rebuilds
the instance, which re-derives. Deferred past ``__init__`` because during a
config load this router can be constructed before its tier deployments are.
``None`` when this router was built with ``derive_savings_baseline=False``.
Derived once on first use and pinned for the instance's lifetime: creating or
editing the router rebuilds the instance, which re-derives. Deferred past
``__init__`` because during a config load this router can be constructed
before its tier deployments are.
"""
import litellm
from litellm.router_strategy.savings_baseline import resolve_baseline
if not self._derive_savings_baseline or litellm.autorouter_savings_baseline_model is not None:
if not self._derive_savings_baseline:
return None
if not self._savings_baseline_derived:
self._savings_baseline = resolve_baseline(self.litellm_router_instance, self._hardest_tier_models())

View file

@ -1,16 +1,13 @@
"""The default counterfactual a complexity router's savings are measured against.
"""The counterfactual a complexity router's savings are measured against.
`litellm_settings.autorouter_savings_baseline_model` names the model the traffic would
have run on without a router. When the operator sets it, that answer wins and nothing
here runs. When they do not, the router's own tier ladder already names it: without a
router a deployment has to pick one model that can carry the hardest request it will
see, so the default baseline is the priciest model in the hardest configured tier. A
cheap tier is a choice the router made, not a ceiling it was bounded by.
The router's own tier ladder names the model the traffic would have run on without a
router: a deployment has to pick one model that can carry the hardest request it will
see, so the baseline is the priciest model in the hardest configured tier. A cheap
tier is a choice the router made, not a ceiling it was bounded by.
Candidates are ranked once against a fixed reference request, not against each request
that runs. Ranking per request means reading the request, and every input shape it can
take; a default must not carry that surface. An operator whose pool ordering genuinely
depends on request shape names the baseline in config, which skips this file entirely.
take; a per-router default must not carry that surface.
Baselines are always provider-qualified, because they travel to the spend writer as a
bare string with no provider beside them; an operator who writes ``deepseek-r1`` meaning

View file

@ -246,11 +246,9 @@ class TestFlush:
class TestEnqueueSeam:
@pytest.mark.asyncio
async def test_update_database_seam_enqueues_only_auto_routed_success(self, monkeypatch: pytest.MonkeyPatch):
import litellm
from litellm.proxy.db.db_spend_update_writer import DBSpendUpdateWriter
from litellm.proxy.utils import PrismaClient
monkeypatch.setattr(litellm, "autorouter_savings_baseline_model", None)
monkeypatch.setattr(PrismaClient, "autorouter_turn_transactions", [])
writer = DBSpendUpdateWriter()
fake_prisma = type("P", (), {})()

View file

@ -540,16 +540,15 @@ def test_autorouter_savings_zero_without_baseline():
assert result.autorouter == 0.0
def test_compute_savings_spend_carries_a_losing_switch_through(monkeypatch):
def test_compute_savings_spend_carries_a_losing_switch_through():
"""The signed value must survive into SavingsSpend; clamping it here would put the
dashboard back to only ever showing gains."""
monkeypatch.setattr(litellm, "autorouter_savings_baseline_model", "claude-sonnet-5")
result = compute_savings_spend(
model="claude-haiku-4-5",
custom_llm_provider="anthropic",
compression_saved_tokens=0,
gateway_injected_cache=True,
routing_decision={"conversation_continuing": True},
routing_decision={"conversation_continuing": True, "savings_baseline_model": "anthropic/claude-sonnet-5"},
usage_object=_cached_usage_object(),
)
assert result.autorouter < 0
@ -911,35 +910,25 @@ def test_a_baseline_recorded_on_the_decision_turns_the_driver_on():
assert result.autorouter != 0.0
def test_the_configured_baseline_overrides_the_recorded_one(monkeypatch):
"""The recorded baseline and its deployment id are both ignored under the setting."""
monkeypatch.setattr(litellm, "autorouter_savings_baseline_model", "claude-sonnet-5")
with_override = compute_savings_spend(
def test_a_leftover_configured_baseline_does_not_override_the_recorded_one(monkeypatch):
"""The proxy config loader setattrs unknown litellm_settings keys, so a stale
autorouter_savings_baseline_model key must stay inert."""
monkeypatch.setattr(litellm, "autorouter_savings_baseline_model", "claude-sonnet-5", raising=False)
result = compute_savings_spend(
model="claude-haiku-4-5",
custom_llm_provider="anthropic",
compression_saved_tokens=0,
gateway_injected_cache=True,
routing_decision={
"conversation_continuing": True,
"savings_baseline_model": "anthropic/claude-opus-5",
"savings_baseline_deployment_id": "some-deployment-id",
},
routing_decision={"conversation_continuing": True, "savings_baseline_model": "anthropic/claude-opus-5"},
usage_object=_cached_usage_object(),
)
against_sonnet = compute_autorouter_savings(
baseline_model="claude-sonnet-5",
selected_model="claude-haiku-4-5",
selected_provider="anthropic",
usage=Usage(**_cached_usage_object()),
)
against_opus = compute_autorouter_savings(
baseline_model="anthropic/claude-opus-5",
selected_model="claude-haiku-4-5",
selected_provider="anthropic",
usage=Usage(**_cached_usage_object()),
)
assert against_sonnet != against_opus, "the test needs baselines that price apart"
assert with_override.autorouter == against_sonnet
assert result.autorouter == against_opus
def test_a_non_string_recorded_baseline_is_ignored():

View file

@ -7934,10 +7934,12 @@ class TestSavingsBaselineOnDecision:
router = self._router_with_tiers({"SIMPLE": "cheap", "MEDIUM": "mid"})
assert router.savings_baseline.model == "anthropic/claude-sonnet-5"
def test_a_configured_proxy_wide_baseline_disables_derivation(self, monkeypatch):
monkeypatch.setattr(litellm, "autorouter_savings_baseline_model", "claude-opus-5")
def test_a_leftover_proxy_wide_baseline_setting_does_not_disable_derivation(self, monkeypatch):
"""The proxy config loader setattrs unknown litellm_settings keys, so a stale
autorouter_savings_baseline_model key must stay inert."""
monkeypatch.setattr(litellm, "autorouter_savings_baseline_model", "claude-opus-5", raising=False)
router = self._router_with_tiers({"SIMPLE": "cheap", "REASONING": "top"})
assert router.savings_baseline is None
assert router.savings_baseline.model == "anthropic/claude-fable-5"
def test_the_decision_record_carries_the_derived_baseline_and_its_deployment(self):
"""The deployment id is what lets the spend writer price a baseline whose
@ -8011,12 +8013,6 @@ class TestSavingsBaselinePinnedPerInstance:
)
assert rebuilt.savings_baseline is None
def test_the_configured_setting_bypasses_the_pin(self, monkeypatch):
router, _ = self._router_and_parent()
assert router.savings_baseline.model == "anthropic/claude-sonnet-5"
monkeypatch.setattr(litellm, "autorouter_savings_baseline_model", "claude-opus-5")
assert router.savings_baseline is None
def test_an_unresolvable_pool_is_derived_once_and_pinned_as_none(self):
router, parent = self._router_and_parent()
parent.model_name_to_deployment_indices.clear()