fix(cost): honor an explicit zero tier rate instead of the fallback

A tier that prices cached reads or reasoning tokens at 0.0 was billed at
the fallback rate because calculate_tiered_cost and tier_rate resolved the
per-token cost with `tier.get(cost_key) or tier.get(fallback_cost_key, 0)`,
whose `or` short-circuits on a falsy 0.0. Distinguish a missing key from a
present zero so a free-cache-read or free-reasoning tier is billed at 0.

This restores the behavior of #30749, which fixed the same short-circuit in
the dashscope calculator before the logic moved into this shared helper.
This commit is contained in:
Henry Su 2026-08-12 16:11:39 -05:00
parent 32535987e8
commit 4da82eb33d
No known key found for this signature in database
2 changed files with 86 additions and 6 deletions

View file

@ -25,6 +25,17 @@ def _coerce_cost_per_token(value: float | str | None) -> float:
return float(value)
def _resolve_tier_cost_per_token(
tier: dict,
cost_key: str,
fallback_cost_key: str | None,
) -> float:
primary: Final = tier.get(cost_key)
if primary is not None:
return _coerce_cost_per_token(primary)
return _coerce_cost_per_token(tier.get(fallback_cost_key, 0))
def calculate_tiered_cost(
tokens: int,
tiered_pricing: list[dict],
@ -84,8 +95,7 @@ def calculate_tiered_cost(
if tier_end > tier_start:
tokens_in_tier = tier_end - tier_start
cost_per_token = tier.get(cost_key) or tier.get(fallback_cost_key, 0)
total_cost += tokens_in_tier * _coerce_cost_per_token(cost_per_token)
total_cost += tokens_in_tier * _resolve_tier_cost_per_token(tier, cost_key, fallback_cost_key)
tokens_processed = tier_end
# After loop, check if any tokens remain (i.e., tokens > highest tier's end range)
@ -93,8 +103,7 @@ def calculate_tiered_cost(
if tokens_processed < tokens and sorted_tiers:
last_tier: Final = sorted_tiers[-1]
remaining_tokens: Final = tokens - tokens_processed
cost_per_token = last_tier.get(cost_key) or last_tier.get(fallback_cost_key, 0)
total_cost += remaining_tokens * _coerce_cost_per_token(cost_per_token)
total_cost += remaining_tokens * _resolve_tier_cost_per_token(last_tier, cost_key, fallback_cost_key)
return total_cost
@ -135,5 +144,4 @@ def tier_rate(
fallback_cost_key: str | None = None,
) -> float:
"""Read a per-token rate from a tier, coercing YAML string costs to float."""
raw: Final = tier.get(cost_key) or tier.get(fallback_cost_key, 0)
return _coerce_cost_per_token(raw)
return _resolve_tier_cost_per_token(tier, cost_key, fallback_cost_key)

View file

@ -0,0 +1,72 @@
import os
import sys
sys.path.insert(0, os.path.abspath("../../.."))
from litellm.litellm_core_utils.llm_cost_calc.tiered_pricing import (
calculate_tiered_cost,
tier_rate,
)
def test_calculate_tiered_cost_honors_explicit_zero_primary_rate():
tiers = [
{
"range": [0, 100000],
"input_cost_per_token": 1e-6,
"cache_read_input_token_cost": 0.0,
}
]
cost = calculate_tiered_cost(
tokens=10000,
tiered_pricing=tiers,
cost_key="cache_read_input_token_cost",
fallback_cost_key="input_cost_per_token",
)
assert cost == 0.0
def test_calculate_tiered_cost_honors_explicit_zero_primary_rate_in_overflow_tier():
tiers = [
{
"range": [0, 100000],
"input_cost_per_token": 1e-6,
"cache_read_input_token_cost": 0.0,
}
]
cost = calculate_tiered_cost(
tokens=150000,
tiered_pricing=tiers,
cost_key="cache_read_input_token_cost",
fallback_cost_key="input_cost_per_token",
)
assert cost == 0.0
def test_calculate_tiered_cost_falls_back_when_primary_rate_is_missing():
tiers = [{"range": [0, 100000], "input_cost_per_token": 1e-6}]
cost = calculate_tiered_cost(
tokens=10000,
tiered_pricing=tiers,
cost_key="cache_read_input_token_cost",
fallback_cost_key="input_cost_per_token",
)
assert cost == 10000 * 1e-6
def test_tier_rate_honors_explicit_zero_primary_rate():
tier = {"cache_read_input_token_cost": 0.0, "input_cost_per_token": 1e-6}
assert tier_rate(tier, "cache_read_input_token_cost", "input_cost_per_token") == 0.0
def test_tier_rate_falls_back_when_primary_rate_is_missing():
tier = {"input_cost_per_token": 1e-6}
assert tier_rate(tier, "cache_read_input_token_cost", "input_cost_per_token") == 1e-6