fix(cost): bill reasoning tokens at the service tier output rate

A tier request against a model that publishes tier output pricing but no
tier reasoning key (every current Gemini flash entry) billed reasoning
tokens at the standard output_cost_per_reasoning_token, undercounting
priority and fast traffic where thinking tokens dominate completions

generic_cost_per_token now resolves the reasoning rate with explicit
precedence: an explicit output_cost_per_reasoning_token_<tier> key wins,
then the tier-resolved output rate when the model prices that tier, then
the standard reasoning key, then the output base cost. The two tier
reasoning keys are wired through ModelInfo so providers can publish real
tiered reasoning prices when they exist
This commit is contained in:
mateo-berri 2026-08-05 00:15:52 -07:00
parent 732bba00df
commit e0833c4ba3
5 changed files with 154 additions and 3 deletions

View file

@ -681,6 +681,23 @@ def _get_regional_uplift_multiplier(model_info: ModelInfo, data_residency: str |
return 1.0
def _resolve_reasoning_token_cost(
model_info: ModelInfo,
service_tier: str | None,
completion_base_cost: float,
) -> float:
tier_reasoning_key: Final = _get_service_tier_cost_key("output_cost_per_reasoning_token", service_tier)
if model_info.get(tier_reasoning_key) is not None:
tier_reasoning_cost: Final = _get_cost_per_unit(model_info, tier_reasoning_key, None)
if tier_reasoning_cost is not None:
return tier_reasoning_cost
tier_output_key: Final = _get_service_tier_cost_key("output_cost_per_token", service_tier)
if tier_output_key != "output_cost_per_token" and model_info.get(tier_output_key) is not None:
return completion_base_cost
standard_reasoning_cost: Final = _get_cost_per_unit(model_info, "output_cost_per_reasoning_token", None)
return standard_reasoning_cost if standard_reasoning_cost is not None else completion_base_cost
def generic_cost_per_token(
model: str,
usage: Usage,
@ -817,9 +834,10 @@ def generic_cost_per_token(
## REASONING COST
if not is_text_tokens_total and reasoning_tokens and reasoning_tokens > 0:
_output_cost_per_reasoning_token = _get_cost_per_unit(model_info, "output_cost_per_reasoning_token", None)
_output_cost_per_reasoning_token = (
_output_cost_per_reasoning_token if _output_cost_per_reasoning_token is not None else completion_base_cost
_output_cost_per_reasoning_token = _resolve_reasoning_token_cost(
model_info=model_info,
service_tier=service_tier,
completion_base_cost=completion_base_cost,
)
completion_cost += float(reasoning_tokens) * _output_cost_per_reasoning_token

View file

@ -263,6 +263,8 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
output_cost_per_video_token: Optional[float] # for gemini omni models with video output
output_vector_size: Optional[int]
output_cost_per_reasoning_token: Optional[float]
output_cost_per_reasoning_token_flex: Optional[float]
output_cost_per_reasoning_token_priority: Optional[float]
output_cost_per_video_per_second: Optional[float] # only for vertex ai models
output_cost_per_audio_per_second: Optional[float] # only for vertex ai models
output_cost_per_second: Optional[float] # for OpenAI Speech models
@ -3308,6 +3310,8 @@ class CustomPricingLiteLLMParams(BaseModel):
output_cost_per_image_token: Optional[float] = None
output_cost_per_video_token: Optional[float] = None
output_cost_per_reasoning_token: Optional[float] = None
output_cost_per_reasoning_token_flex: Optional[float] = None
output_cost_per_reasoning_token_priority: Optional[float] = None
output_cost_per_video_per_second: Optional[float] = None
output_cost_per_audio_per_second: Optional[float] = None
search_context_cost_per_query: Optional[Dict[str, Any]] = None

View file

@ -5482,6 +5482,10 @@ def _get_model_info_helper(
output_cost_per_audio_token=_model_info.get("output_cost_per_audio_token", None),
output_cost_per_character=_model_info.get("output_cost_per_character", None),
output_cost_per_reasoning_token=_model_info.get("output_cost_per_reasoning_token", None),
output_cost_per_reasoning_token_flex=_model_info.get("output_cost_per_reasoning_token_flex", None),
output_cost_per_reasoning_token_priority=_model_info.get(
"output_cost_per_reasoning_token_priority", None
),
output_cost_per_token_above_128k_tokens=_model_info.get(
"output_cost_per_token_above_128k_tokens", None
),

View file

@ -2553,3 +2553,120 @@ def test_fast_service_tier_matches_priority_above_the_context_threshold(_local_m
assert fast == priority
assert fast[0] == pytest.approx(300_000 * 1e-05, rel=1e-9)
assert fast[1] == pytest.approx(1_000 * 4.5e-05, rel=1e-9)
def test_priority_reasoning_tokens_bill_at_the_priority_output_rate(_local_model_cost_map):
"""Regression: gemini-3.5-flash publishes priority output pricing but no priority
reasoning key, so reasoning tokens under priority/fast were billed at the standard
output_cost_per_reasoning_token instead of following the tier's output rate."""
from litellm.types.utils import Usage
usage = Usage(
prompt_tokens=1_000,
completion_tokens=5_000,
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=4_000),
)
model_info = litellm.get_model_info(model="gemini-3.5-flash", custom_llm_provider="gemini")
standard_output_rate = model_info["output_cost_per_token"]
standard_reasoning_rate = model_info["output_cost_per_reasoning_token"]
priority_output_rate = model_info["output_cost_per_token_priority"]
assert priority_output_rate is not None
assert priority_output_rate != standard_reasoning_rate
standard = generic_cost_per_token(
model="gemini-3.5-flash", usage=usage, custom_llm_provider="gemini", service_tier=None
)
priority = generic_cost_per_token(
model="gemini-3.5-flash", usage=usage, custom_llm_provider="gemini", service_tier="priority"
)
fast = generic_cost_per_token(
model="gemini-3.5-flash", usage=usage, custom_llm_provider="gemini", service_tier="fast"
)
assert standard[1] == pytest.approx(1_000 * standard_output_rate + 4_000 * standard_reasoning_rate, rel=1e-9)
assert priority[1] == pytest.approx(5_000 * priority_output_rate, rel=1e-9)
assert fast == priority
def test_explicit_tier_reasoning_key_wins_over_the_tier_output_rate():
from litellm.types.utils import Usage
model_info = {
"input_cost_per_token": 1e-06,
"output_cost_per_token": 4e-06,
"output_cost_per_reasoning_token": 6e-06,
"input_cost_per_token_priority": 2e-06,
"output_cost_per_token_priority": 8e-06,
"output_cost_per_reasoning_token_priority": 1.2e-05,
}
usage = Usage(
prompt_tokens=100,
completion_tokens=1_000,
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=600),
)
_, completion_cost = generic_cost_per_token(
model="synthetic-model",
usage=usage,
custom_llm_provider="openai",
service_tier="priority",
model_info=model_info,
)
assert completion_cost == pytest.approx(400 * 8e-06 + 600 * 1.2e-05, rel=1e-9)
def test_null_tier_reasoning_key_falls_back_to_the_tier_output_rate():
"""get_model_info dumps every ModelInfo field, so an unpublished tier reasoning key
arrives as an explicit None and must not shadow the tier output rate."""
from litellm.types.utils import Usage
model_info = {
"input_cost_per_token": 1e-06,
"output_cost_per_token": 4e-06,
"output_cost_per_reasoning_token": 6e-06,
"output_cost_per_reasoning_token_priority": None,
"input_cost_per_token_priority": 2e-06,
"output_cost_per_token_priority": 8e-06,
}
usage = Usage(
prompt_tokens=100,
completion_tokens=1_000,
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=600),
)
_, completion_cost = generic_cost_per_token(
model="synthetic-model",
usage=usage,
custom_llm_provider="openai",
service_tier="priority",
model_info=model_info,
)
assert completion_cost == pytest.approx(1_000 * 8e-06, rel=1e-9)
def test_tier_request_without_tier_pricing_keeps_the_standard_reasoning_rate():
from litellm.types.utils import Usage
model_info = {
"input_cost_per_token": 1e-06,
"output_cost_per_token": 4e-06,
"output_cost_per_reasoning_token": 6e-06,
}
usage = Usage(
prompt_tokens=100,
completion_tokens=1_000,
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=600),
)
_, completion_cost = generic_cost_per_token(
model="synthetic-model",
usage=usage,
custom_llm_provider="openai",
service_tier="priority",
model_info=model_info,
)
assert completion_cost == pytest.approx(400 * 4e-06 + 600 * 6e-06, rel=1e-9)

View file

@ -26561,6 +26561,10 @@ export interface components {
output_cost_per_pixel?: number | null;
/** Output Cost Per Reasoning Token */
output_cost_per_reasoning_token?: number | null;
/** Output Cost Per Reasoning Token Flex */
output_cost_per_reasoning_token_flex?: number | null;
/** Output Cost Per Reasoning Token Priority */
output_cost_per_reasoning_token_priority?: number | null;
/** Output Cost Per Second */
output_cost_per_second?: number | null;
/** Output Cost Per Second 1080P */
@ -35120,6 +35124,10 @@ export interface components {
output_cost_per_pixel?: number | null;
/** Output Cost Per Reasoning Token */
output_cost_per_reasoning_token?: number | null;
/** Output Cost Per Reasoning Token Flex */
output_cost_per_reasoning_token_flex?: number | null;
/** Output Cost Per Reasoning Token Priority */
output_cost_per_reasoning_token_priority?: number | null;
/** Output Cost Per Second */
output_cost_per_second?: number | null;
/** Output Cost Per Second 1080P */