Include multimodal tokens in tiered pricing threshold

This commit is contained in:
its-amann 2026-06-12 18:29:24 +05:30 • committed by Aman Agnihotri
parent cd2672a448
commit b0e2852f25
2 changed files with 78 additions and 5 deletions

View file

@ -616,6 +616,7 @@ def _get_effective_prompt_tokens_for_tiered_pricing(usage: Usage) -> float:
them separate. When prompt_tokens_details is present, prefer the category
fields so the threshold check can avoid double-counting cache tokens.
"""
prompt_tokens = float(getattr(usage, "prompt_tokens", 0) or 0)
if usage.prompt_tokens_details is not None:
details = usage.prompt_tokens_details
raw_text_tokens = getattr(details, "text_tokens", None)
@ -626,17 +627,29 @@ def _get_effective_prompt_tokens_for_tiered_pricing(usage: Usage) -> float:
# If prompt_tokens already covers the cache detail total, use it as
# the rolled-up input total. Otherwise cache tokens were reported
# outside prompt_tokens and must be added for the tier threshold.
prompt_tokens = float(getattr(usage, "prompt_tokens", 0) or 0)
cache_detail_tokens = cached_tokens + cache_creation
if cache_detail_tokens > 0 and prompt_tokens < cache_detail_tokens:
return prompt_tokens + cache_detail_tokens
return prompt_tokens
text_tokens = float(raw_text_tokens or 0)
return text_tokens + cached_tokens + cache_creation
audio_tokens = float(getattr(details, "audio_tokens", 0) or 0)
image_tokens = float(getattr(details, "image_tokens", 0) or 0)
video_tokens = float(getattr(details, "video_tokens", 0) or 0)
detail_total = (
text_tokens
+ audio_tokens
+ image_tokens
+ video_tokens
+ cached_tokens
+ cache_creation
)
cache_detail_tokens = cached_tokens + cache_creation
if cache_detail_tokens > 0 and detail_total > prompt_tokens:
return prompt_tokens
return max(prompt_tokens, detail_total)
# No prompt_tokens_details. Add explicit cache fields only if they are
# not already rolled into prompt_tokens (determined by their presence).
prompt_tokens = float(getattr(usage, "prompt_tokens", 0) or 0)
cache_read_tokens = float(getattr(usage, "cache_read_input_tokens", 0) or 0)
cache_creation_tokens = float(getattr(usage, "cache_creation_input_tokens", 0) or 0)
return prompt_tokens + cache_read_tokens + cache_creation_tokens
@ -1140,8 +1153,11 @@ def _calculate_input_cost(
# First check if input_cost_per_image_token is available. If not, default to generic input_cost_per_token.
image_token_cost_key = "input_cost_per_image_token"
if model_info.get(image_token_cost_key) is None:
image_token_cost_key = "input_cost_per_token"
prompt_cost += calculate_cost_component(model_info, image_token_cost_key, prompt_tokens_details["image_tokens"])
prompt_cost += float(prompt_tokens_details["image_tokens"]) * prompt_base_cost
else:
prompt_cost += calculate_cost_component(
model_info, image_token_cost_key, prompt_tokens_details["image_tokens"]
)
### VIDEO TOKEN COST
if prompt_tokens_details["video_tokens"] and not (

View file

@ -2661,6 +2661,63 @@ def test_custom_pricing_does_not_double_count_cached_tokens_when_text_tokens_mis
litellm.model_cost.pop(f"anthropic/{model_id}", None)
def test_custom_pricing_uses_tiered_rate_when_multimodal_details_cross_threshold():
"""
Multimodal prompt detail tokens should count toward tier thresholds even
when text_tokens is present.
"""
model_id = "tiered-custom-pricing-multimodal-details-test"
model_info = {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 0.000005,
"output_cost_per_token": 0.000025,
"input_cost_per_token_above_200k_tokens": 0.00001,
"output_cost_per_token_above_200k_tokens": 0.0000375,
}
text_tokens = 150_000
image_tokens = 60_001
litellm.register_model(model_cost={model_id: model_info})
try:
response = ModelResponse(
id="test-id",
created=1234567890,
model=f"openai/{model_id}",
object="chat.completion",
choices=[],
usage=Usage(
prompt_tokens=text_tokens + image_tokens,
completion_tokens=0,
total_tokens=text_tokens + image_tokens,
prompt_tokens_details=PromptTokensDetailsWrapper(
text_tokens=text_tokens,
image_tokens=image_tokens,
),
),
)
cost = completion_cost(
completion_response=response,
model=f"openai/{model_id}",
custom_llm_provider="openai",
custom_pricing=True,
router_model_id=model_id,
)
expected_cost = (
text_tokens + image_tokens
) * model_info["input_cost_per_token_above_200k_tokens"]
base_cost = (text_tokens + image_tokens) * model_info["input_cost_per_token"]
assert cost == pytest.approx(expected_cost)
assert cost != pytest.approx(base_cost)
finally:
litellm.model_cost.pop(model_id, None)
litellm.model_cost.pop(f"openai/{model_id}", None)
def test_extract_cache_read_tokens_anthropic_top_level():
from litellm.proxy.spend_tracking.savings import extract_cache_read_tokens as _extract_cache_read_tokens