Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions litellm/litellm_core_utils/llm_cost_calc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Tiered reasoning charges bypass budget reservations

This settles reasoning tokens at the tier output rate, while _max_cost_for_cost_info() reserves output using only output_cost_per_token and output_cost_per_reasoning_token and ignores the request's service tier. For example, a priority gemini-3.5-flash request can be reserved at 9e-6 per output token but settle at 1.62e-5, allowing a user to submit a reasoning-heavy request that is admitted below the remaining budget and then exceeds it. Update budget reservation to resolve the requested tier's output and reasoning keys and reserve every output token at the highest applicable rate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reservation already ignored service_tier for input and output tokens; making it tier-aware is a separate estimator change covering every token type at once

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,
Expand Down Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions litellm/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
output_cost_per_video_token: float | None # for gemini omni models with video output
output_vector_size: int | None
output_cost_per_reasoning_token: float | None
output_cost_per_reasoning_token_flex: float | None
output_cost_per_reasoning_token_priority: float | None
output_cost_per_video_per_second: float | None # only for vertex ai models
output_cost_per_audio_per_second: float | None # only for vertex ai models
output_cost_per_second: float | None # for OpenAI Speech models
Expand Down Expand Up @@ -3308,6 +3310,8 @@ class CustomPricingLiteLLMParams(BaseModel):
output_cost_per_image_token: float | None = None
output_cost_per_video_token: float | None = None
output_cost_per_reasoning_token: float | None = None
output_cost_per_reasoning_token_flex: float | None = None
output_cost_per_reasoning_token_priority: float | None = None
output_cost_per_video_per_second: float | None = None
output_cost_per_audio_per_second: float | None = None
search_context_cost_per_query: dict[str, Any] | None = None
Expand Down
4 changes: 4 additions & 0 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5533,6 +5533,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
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2620,3 +2620,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)
8 changes: 8 additions & 0 deletions ui/litellm-dashboard/src/lib/http/schema.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading