fix(budget_reservation): fall back to the model's output rate for input-only tiers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-27 05:54:00 +00:00
parent afef2a0e2f
commit d3c8b5d44d
2 changed files with 57 additions and 6 deletions

View file

@ -1045,6 +1045,21 @@ def _estimate_request_max_cost_for_model(
return max(valid_estimates) if valid_estimates else None
_TIER_OUTPUT_RATE_KEYS: Final = ("output_cost_per_token", "output_cost_per_reasoning_token")
def _tier_output_rate(tier: Mapping[str, object], model_info: Mapping[str, object]) -> float:
"""Output rate to reserve for a request billed at ``tier``.
A tier table that prices only input falls back to the model's own output rates when
the request is billed, so reserving the tier's missing rate as 0 leaves every
completion under-reserved. The reasoning-token share is unknown before the request
runs, so the higher of the two rates is used either way.
"""
rates: Final = tier if any(key in tier for key in _TIER_OUTPUT_RATE_KEYS) else model_info
return max(_to_float(rates.get(key)) or 0.0 for key in _TIER_OUTPUT_RATE_KEYS)
def _max_cost_for_cost_info(
request_body: dict,
route: str,
@ -1079,12 +1094,8 @@ def _max_cost_for_cost_info(
if isinstance(tiered_pricing, list) and tiered_pricing:
tier: Final = select_tier_for_input(tiered_pricing=tiered_pricing, input_tokens=estimated_input_tokens)
if tier is not None:
output_rate = max(
tier_rate(tier, "output_cost_per_token"),
tier_rate(tier, "output_cost_per_reasoning_token"),
)
return (estimated_input_tokens * tier_rate(tier, "input_cost_per_token")) + (
output_tokens * output_multiplier * output_rate
output_tokens * output_multiplier * _tier_output_rate(tier=tier, model_info=model_info)
)
input_cost_per_token: Final = _to_float(model_info.get("input_cost_per_token"))

View file

@ -1124,7 +1124,7 @@ def test_reservation_uses_most_expensive_deployment_in_group():
],
ids=["litellm_params", "model_info"],
)
def test_free_deployment_of_tiered_model_reserves_nothing(deployment_overrides):
def test_free_deployment_of_tiered_model_reserves_nothing(deployment_overrides: dict[str, dict[str, int]]):
"""A deployment priced at 0 on a model whose published entry carries a tier table
must not be estimated against that table. Spend tracking bills such a deployment at
its own rates, so reserving the published tier rate consumed, and rejected requests
@ -1223,6 +1223,46 @@ def test_deployment_declaring_own_tier_table_keeps_it():
assert estimated is not None and estimated > 0
def test_input_only_tier_reserves_the_models_own_output_rate():
"""A tier table that prices only input is billed with the model's own output rates,
so reserving the tier's absent output rate as 0 would leave every completion
unreserved and let a budgeted caller run past their limit."""
output_tokens = 500
router = Router(
model_list=[
{
"model_name": "input-tiered",
"litellm_params": {
"model": "dashscope/qwen-plus-latest",
"api_key": "sk-fake",
"input_cost_per_token": 1e-06,
"output_cost_per_token": 5e-06,
"tiered_pricing": [{"range": [0, 32000], "input_cost_per_token": 2e-06}],
},
}
]
)
request_body = {
"model": "input-tiered",
"messages": [{"role": "user", "content": "hello " * 100}],
"max_tokens": output_tokens,
}
input_cost = estimate_request_input_cost(
request_body=request_body,
route="/chat/completions",
llm_router=router,
)
estimated = estimate_request_max_cost(
request_body=request_body,
route="/chat/completions",
llm_router=router,
)
assert input_cost is not None and input_cost > 0
assert estimated == pytest.approx(input_cost + (output_tokens * 5e-06))
@pytest.mark.asyncio
async def test_should_clamp_reservation_to_model_ceiling_when_caller_overrequests(
spend_counter_state,