mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
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:
parent
3fa4a4b72a
commit
fb80ba7c98
7 changed files with 28 additions and 54 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", (), {})()
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue