diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index d31df2a82a5..12ad598f0e7 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -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( diff --git a/tests/test_litellm/test_default_image_cost_calculator.py b/tests/test_litellm/test_default_image_cost_calculator.py index 1cc5c0eb328..68a930533f1 100644 --- a/tests/test_litellm/test_default_image_cost_calculator.py +++ b/tests/test_litellm/test_default_image_cost_calculator.py @@ -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