fix(image_cost): split cached_tokens text-first to avoid double-bill

This commit is contained in:
kimsehwan96 2026-04-30 19:34:25 +09:00
parent 5ee982b059
commit fab911f233
2 changed files with 18 additions and 17 deletions

View file

@ -1925,17 +1925,14 @@ def _image_cost_from_token_usage(
image_in = _detail("prompt_tokens_details", "image_tokens")
image_out = _detail("completion_tokens_details", "image_tokens")
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"
text_cached = min(text_in, cached_in)
image_cached = cached_in - text_cached
rates: List[Tuple[str, int]] = [
("input_cost_per_token", text_in_uncached),
(cache_rate_key, cached_in),
("input_cost_per_image_token", image_in),
("input_cost_per_token", text_in - text_cached),
("cache_read_input_token_cost", text_cached),
("input_cost_per_image_token", image_in - image_cached),
("cache_read_input_image_token_cost", image_cached),
("output_cost_per_image_token", image_out),
]
if not any(cost_info.get(key) is not None for key, _ in rates):

View file

@ -206,11 +206,13 @@ 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).
def test_token_fallback_splits_cached_tokens_between_text_and_image(
self, monkeypatch
):
"""Image-edit responses report a single ``cached_tokens`` count.
Charge text input first against the standard cache rate; the
remainder is billed at the dedicated image cache rate. Avoids
double-billing the text portion at the image cache rate.
"""
monkeypatch.setitem(
litellm.model_cost,
@ -226,6 +228,8 @@ class TestDefaultImageCostCalculator:
},
)
# cached_in (300) > text_in (10) so 10 are charged at the text cache
# rate and the remaining 290 at the image cache rate.
cost = default_image_cost_calculator(
model="openai/synthetic-image-cache-model",
custom_llm_provider="openai",
@ -233,11 +237,11 @@ class TestDefaultImageCostCalculator:
n=1,
size="1280x720",
image_response=_image_response(
text_in=510, image_in=1452, cached_in=300, image_out=5488
text_in=10, 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
# text uncached: 0, text cache: 10, image uncached: 1162, image cache: 290
expected = 0 * 5e-6 + 10 * 1.25e-6 + 1162 * 8e-6 + 290 * 2e-6 + 5488 * 3e-5
assert abs(cost - expected) < 1e-9
def test_token_fallback_returns_zero_for_free_model(self, monkeypatch):