fix(image_cost): apply image cache rate for edits, return 0.0 for free models

This commit is contained in:
kimsehwan96 2026-04-30 19:25:03 +09:00
parent 4ad6d201b1
commit 5ee982b059
2 changed files with 72 additions and 5 deletions

View file

@ -1905,7 +1905,8 @@ def _image_cost_from_token_usage(
) -> Optional[float]:
"""Token-based image cost from ``cost_info`` + ``image_response.usage``.
Returns ``None`` when no token-cost keys match any non-zero token count.
Returns ``None`` only when ``cost_info`` declares no recognised per-token
rate keys; zero-cost models that explicitly declare zero rates return ``0.0``.
"""
usage = getattr(image_response, "usage", None)
if usage is None:
@ -1923,16 +1924,23 @@ def _image_cost_from_token_usage(
cached_in = _detail("prompt_tokens_details", "cached_tokens")
image_in = _detail("prompt_tokens_details", "image_tokens")
image_out = _detail("completion_tokens_details", "image_tokens")
text_in_uncached = max(text_in - cached_in, 0)
if image_in > 0 and cost_info.get("cache_read_input_image_token_cost") is not None:
text_in_uncached = text_in
cache_rate_key = "cache_read_input_image_token_cost"
else:
text_in_uncached = max(text_in - cached_in, 0)
cache_rate_key = "cache_read_input_token_cost"
rates: List[Tuple[str, int]] = [
("input_cost_per_token", text_in_uncached),
("cache_read_input_token_cost", cached_in),
(cache_rate_key, cached_in),
("input_cost_per_image_token", image_in),
("output_cost_per_image_token", image_out),
]
cost = sum((cost_info.get(key) or 0) * tokens for key, tokens in rates)
return cost if cost > 0 else None
if not any(cost_info.get(key) is not None for key, _ in rates):
return None
return sum((cost_info.get(key) or 0) * tokens for key, tokens in rates)
def default_image_cost_calculator(

View file

@ -206,6 +206,65 @@ class TestDefaultImageCostCalculator:
expected = 40 * 5e-6 + 160 * 1.25e-6 + 600 * 3e-5
assert abs(cost - expected) < 1e-9
def test_token_fallback_uses_image_cache_rate_when_declared(self, monkeypatch):
"""Image-edit responses report a single ``cached_tokens`` count;
when the model declares ``cache_read_input_image_token_cost`` and
image input tokens are present, that rate is used (and text input
is billed in full).
"""
monkeypatch.setitem(
litellm.model_cost,
"synthetic-image-cache-model",
{
"input_cost_per_token": 5e-6,
"cache_read_input_token_cost": 1.25e-6,
"input_cost_per_image_token": 8e-6,
"cache_read_input_image_token_cost": 2e-6,
"output_cost_per_image_token": 3e-5,
"litellm_provider": "openai",
"mode": "image_generation",
},
)
cost = default_image_cost_calculator(
model="openai/synthetic-image-cache-model",
custom_llm_provider="openai",
quality="high",
n=1,
size="1280x720",
image_response=_image_response(
text_in=510, image_in=1452, cached_in=300, image_out=5488
),
)
# text fully uncached, cached_tokens charged at image cache rate
expected = 510 * 5e-6 + 300 * 2e-6 + 1452 * 8e-6 + 5488 * 3e-5
assert abs(cost - expected) < 1e-9
def test_token_fallback_returns_zero_for_free_model(self, monkeypatch):
"""A model that declares zero per-token rates is genuinely free —
the fallback must return ``0.0`` rather than raising.
"""
monkeypatch.setitem(
litellm.model_cost,
"synthetic-free-image-model",
{
"input_cost_per_token": 0.0,
"output_cost_per_image_token": 0.0,
"litellm_provider": "openai",
"mode": "image_generation",
},
)
cost = default_image_cost_calculator(
model="openai/synthetic-free-image-model",
custom_llm_provider="openai",
quality="low",
n=1,
size="2048x768",
image_response=_image_response(text_in=25, image_out=772),
)
assert cost == 0.0
def test_unmapped_model_without_image_response_raises(self):
"""Negative: cost map miss + no ``image_response`` to fall back on
preserve the original behaviour of raising rather than silently