mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(dashscope): treat an explicit 0.0 tier cost as a real price, not missing (#30749)
_calculate_tiered_cost resolved a tier's per-token rate with `tier.get(cost_key) or tier.get(fallback_cost_key, 0)`. The `or` short-circuits on a falsy 0.0, so a tier that legitimately prices cached reads (or reasoning tokens) at 0.0 was silently billed at the full fallback rate, at both the in-range and overflow sites. Add _resolve_tier_cost_per_token, which only falls back when the primary key is absent (is None), mirroring the flat-pricing path that already guards correctly. Uses the X | None annotation style to stay within the ruff strict-rule budget. Re-submit of #30653, which was reverted from litellm_internal_staging because the previous Optional[str] annotation pushed the UP045 count over the ruff-strict-budget.json ceiling.
This commit is contained in:
parent
341e4f2487
commit
c5b7fc5d22
2 changed files with 74 additions and 3 deletions
|
|
@ -42,6 +42,23 @@ def _extract_token_breakdown(usage: Usage) -> TokenBreakdown:
|
|||
return TokenBreakdown(text_tokens, cached_tokens, completion_tokens, reasoning_tokens)
|
||||
|
||||
|
||||
def _resolve_tier_cost_per_token(
|
||||
tier: dict, cost_key: str, fallback_cost_key: str | None
|
||||
) -> float:
|
||||
"""Resolve a tier's per-token cost.
|
||||
|
||||
An explicit 0.0 is a real price (e.g. a free-cache-read tier) and must not
|
||||
be treated as "missing". Only fall back to ``fallback_cost_key`` when the
|
||||
primary key is absent. This mirrors the flat-pricing path in
|
||||
``_calculate_prompt_cost`` / ``_calculate_completion_cost``, which already
|
||||
guards with ``is None``.
|
||||
"""
|
||||
cost = tier.get(cost_key)
|
||||
if cost is None and fallback_cost_key is not None:
|
||||
cost = tier.get(fallback_cost_key)
|
||||
return float(cost) if cost is not None else 0.0
|
||||
|
||||
|
||||
def _calculate_tiered_cost(
|
||||
tokens: int,
|
||||
tiered_pricing: List[dict],
|
||||
|
|
@ -101,7 +118,9 @@ 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)
|
||||
cost_per_token = _resolve_tier_cost_per_token(
|
||||
tier, cost_key, fallback_cost_key
|
||||
)
|
||||
total_cost += tokens_in_tier * cost_per_token
|
||||
tokens_processed = tier_end
|
||||
|
||||
|
|
@ -110,7 +129,9 @@ def _calculate_tiered_cost(
|
|||
if tokens_processed < tokens and sorted_tiers:
|
||||
last_tier = sorted_tiers[-1]
|
||||
remaining_tokens = tokens - tokens_processed
|
||||
cost_per_token = last_tier.get(cost_key) or last_tier.get(fallback_cost_key, 0)
|
||||
cost_per_token = _resolve_tier_cost_per_token(
|
||||
last_tier, cost_key, fallback_cost_key
|
||||
)
|
||||
total_cost += remaining_tokens * cost_per_token
|
||||
|
||||
return total_cost
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ Tests the cost calculation for Dashscope models including:
|
|||
- Correctly calculates costs for token counts exceeding the highest defined tier.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -20,6 +19,7 @@ sys.path.insert(0, os.path.abspath("../../../.."))
|
|||
|
||||
import litellm
|
||||
from litellm.llms.dashscope.cost_calculator import (
|
||||
_calculate_tiered_cost,
|
||||
cost_per_token as dashscope_cost_per_token,
|
||||
)
|
||||
from litellm.types.utils import Usage, PromptTokensDetailsWrapper
|
||||
|
|
@ -157,3 +157,53 @@ class TestDashscopeCostCalculator:
|
|||
)
|
||||
|
||||
assert math.isclose(prompt_cost, expected_prompt_cost, rel_tol=1e-10)
|
||||
|
||||
def test_dashscope_tiered_zero_cost_not_overridden_by_fallback(self):
|
||||
"""
|
||||
A tier may legitimately price cached reads (or reasoning tokens) at 0.0,
|
||||
e.g. a free-cache-read tier. The tiered calculator must treat an explicit
|
||||
0.0 as a real price, not as "missing" and fall back to the full input
|
||||
rate. This mirrors the flat-pricing path, which already guards on `None`.
|
||||
"""
|
||||
tiered_pricing = [
|
||||
{
|
||||
"range": [0, 256000],
|
||||
"input_cost_per_token": 2e-7,
|
||||
"cache_read_input_token_cost": 0.0, # free cache reads in this tier
|
||||
}
|
||||
]
|
||||
|
||||
cost = _calculate_tiered_cost(
|
||||
tokens=10000,
|
||||
tiered_pricing=tiered_pricing,
|
||||
cost_key="cache_read_input_token_cost",
|
||||
fallback_cost_key="input_cost_per_token",
|
||||
)
|
||||
|
||||
# 10,000 cached tokens priced at $0.0/token must cost $0.0, not the
|
||||
# input rate (which would be 10000 * 2e-7 = $0.002).
|
||||
assert cost == 0.0
|
||||
|
||||
def test_dashscope_tiered_zero_cost_applies_to_overflow_tokens(self):
|
||||
"""
|
||||
The same 0.0-is-real-price rule must hold on the overflow path that
|
||||
charges tokens exceeding the highest defined tier at the last tier's
|
||||
rate.
|
||||
"""
|
||||
tiered_pricing = [
|
||||
{
|
||||
"range": [0, 100],
|
||||
"input_cost_per_token": 2e-7,
|
||||
"cache_read_input_token_cost": 0.0,
|
||||
}
|
||||
]
|
||||
|
||||
# 150 cached tokens: 100 in-tier + 50 overflow, both at $0.0.
|
||||
cost = _calculate_tiered_cost(
|
||||
tokens=150,
|
||||
tiered_pricing=tiered_pricing,
|
||||
cost_key="cache_read_input_token_cost",
|
||||
fallback_cost_key="input_cost_per_token",
|
||||
)
|
||||
|
||||
assert cost == 0.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue