fix(cost): inherit the backend output rate when a deployment's tiers omit one

This commit is contained in:
mateo-berri 2026-08-14 20:37:18 -07:00
parent c3e38a0b52
commit 34918d34f9
2 changed files with 86 additions and 0 deletions

View file

@ -7635,6 +7635,37 @@ class Router:
if backend_value is not None:
model_info[field] = backend_value
@staticmethod
def _inherit_builtin_tiered_output_rate(
model_info: dict, backend_model: str, custom_llm_provider: str | None
) -> None:
"""Fill a missing entry-level output rate on a deployment entry whose tier
table omits one, from the backend model's built-in cost map entry.
A deployment's custom pricing is registered as its own standalone
``litellm.model_cost`` entry holding only the supplied fields, and the
tiered-cost output fallback reads that same entry, so a tier table that
spells out only input-side rates would bill every completion at 0.
A user-specified ``output_cost_per_token`` always wins. No-op without a
tier table, when every tier declares its own output rate, or when the
backend model has no canonical entry.
"""
tiers: Final = model_info.get("tiered_pricing")
if not isinstance(tiers, list) or not tiers:
return
if model_info.get("output_cost_per_token") is not None:
return
if all(isinstance(tier, dict) and "output_cost_per_token" in tier for tier in tiers):
return
try:
backend_info: Final = litellm.get_model_info(model=backend_model, custom_llm_provider=custom_llm_provider)
except Exception: # noqa: BLE001 # get_model_info raises plain Exception for an unmapped backend model
return
backend_rate: Final = backend_info.get("output_cost_per_token")
if backend_rate is not None:
model_info["output_cost_per_token"] = backend_rate
def _create_deployment(
self,
deployment_info: dict,
@ -7670,6 +7701,11 @@ class Router:
backend_model=deployment.litellm_params.model,
custom_llm_provider=deployment.litellm_params.custom_llm_provider,
)
Router._inherit_builtin_tiered_output_rate(
model_info=_model_info,
backend_model=deployment.litellm_params.model,
custom_llm_provider=deployment.litellm_params.custom_llm_provider,
)
## REGISTER MODEL INFO IN LITELLM MODEL COST MAP
Router._register_deployment_in_model_cost(
@ -8368,6 +8404,11 @@ class Router:
backend_model=deployment.litellm_params.model,
custom_llm_provider=deployment.litellm_params.custom_llm_provider,
)
Router._inherit_builtin_tiered_output_rate(
model_info=_model_info_dict,
backend_model=deployment.litellm_params.model,
custom_llm_provider=deployment.litellm_params.custom_llm_provider,
)
# Register custom pricing in litellm.model_cost.
# Mirrors _create_deployment() logic to ensure dynamically-added deployments
@ -8598,6 +8639,11 @@ class Router:
backend_model=deployment.litellm_params.model,
custom_llm_provider=deployment.litellm_params.custom_llm_provider,
)
Router._inherit_builtin_tiered_output_rate(
model_info=model_info,
backend_model=deployment.litellm_params.model,
custom_llm_provider=deployment.litellm_params.custom_llm_provider,
)
return model_info
@staticmethod

View file

@ -734,6 +734,46 @@ def test_generic_cost_per_token_tier_without_an_output_rate_bills_the_model_rate
litellm.model_cost.pop(model, None)
def test_router_deployment_with_input_only_tiers_bills_completions_at_the_backend_rate():
"""Regression: the router registers a deployment's custom pricing as a standalone
model_cost entry holding only the supplied fields, so an input-only tier table left
the output-rate fallback nothing to read and billed every completion at 0."""
from litellm import Router
model_id = "litellm-test-router-tiered-input-only"
backend_model = "anthropic/claude-haiku-4-5"
backend_output_rate = litellm.get_model_info(backend_model)["output_cost_per_token"]
Router(
model_list=[
{
"model_name": "tiered-input-only",
"litellm_params": {
"model": backend_model,
"api_key": "sk-test",
"tiered_pricing": [
{"range": [0, 3000], "input_cost_per_token": 3.25e-07},
{"range": [3000, 128000], "input_cost_per_token": 8.125e-07},
],
},
"model_info": {"id": model_id},
}
]
)
try:
usage = Usage(prompt_tokens=21, completion_tokens=4, total_tokens=25)
prompt_cost, completion_cost = generic_cost_per_token(
model=model_id,
usage=usage,
custom_llm_provider="anthropic",
)
assert round(prompt_cost, 12) == round(21 * 3.25e-07, 12)
assert round(completion_cost, 12) == round(4 * backend_output_rate, 12)
assert backend_output_rate > 0
finally:
litellm.model_cost.pop(model_id, None)
def test_generic_cost_per_token_tiered_pricing_bills_reasoning_at_tier_rate():
"""Regression: a tier's output_cost_per_reasoning_token must price reasoning tokens
on the generic path and in the logged breakdown, not the tier's plain output rate."""