mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-20 06:23:46 +00:00
Fix gpt-image-1.5 cost calculation not including output image tokens (#19515)
Fixes #19508 The cost calculation for gpt-image-1.5 was not including image tokens from output_tokens_details, causing costs to be underreported (e.g., $0.046 instead of $0.14). Root cause: The OpenAI image generation API uses Responses API naming (input_tokens, output_tokens, output_tokens_details) but the cost calculator expected Chat Completions API naming (prompt_tokens, completion_tokens, completion_tokens_details). Changes: - convert_dict_to_response.py: Map Responses API fields to Chat Completions API fields and convert dicts to wrapper objects - cost_calculator.py: Use usage directly if already transformed, avoiding double transformation that lost the wrapper objects - Added test for gpt-image-1.5 output image token cost calculation
This commit is contained in:
@@ -21,11 +21,13 @@ from litellm.types.utils import (
|
||||
ChatCompletionMessageToolCall,
|
||||
ChatCompletionRedactedThinkingBlock,
|
||||
Choices,
|
||||
CompletionTokensDetailsWrapper,
|
||||
Delta,
|
||||
EmbeddingResponse,
|
||||
Function,
|
||||
HiddenParams,
|
||||
ImageResponse,
|
||||
PromptTokensDetailsWrapper,
|
||||
)
|
||||
from litellm.types.utils import Logprobs as TextCompletionLogprobs
|
||||
from litellm.types.utils import (
|
||||
@@ -304,6 +306,22 @@ class LiteLLMResponseObjectHandler:
|
||||
"text_tokens": 0,
|
||||
}
|
||||
|
||||
# Map Responses API naming to Chat Completions API naming for cost calculator
|
||||
if usage.get("prompt_tokens") is None:
|
||||
usage["prompt_tokens"] = usage.get("input_tokens", 0)
|
||||
if usage.get("completion_tokens") is None:
|
||||
usage["completion_tokens"] = usage.get("output_tokens", 0)
|
||||
|
||||
# Convert dicts to wrapper objects so getattr() works in cost calculation
|
||||
if isinstance(usage.get("input_tokens_details"), dict):
|
||||
usage["prompt_tokens_details"] = PromptTokensDetailsWrapper(
|
||||
**usage["input_tokens_details"]
|
||||
)
|
||||
if isinstance(usage.get("output_tokens_details"), dict):
|
||||
usage["completion_tokens_details"] = CompletionTokensDetailsWrapper(
|
||||
**usage["output_tokens_details"]
|
||||
)
|
||||
|
||||
if model_response_object is None:
|
||||
model_response_object = ImageResponse(**response_object)
|
||||
return model_response_object
|
||||
|
||||
@@ -8,8 +8,7 @@ from typing import Optional
|
||||
|
||||
from litellm import verbose_logger
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token
|
||||
from litellm.responses.utils import ResponseAPILoggingUtils
|
||||
from litellm.types.utils import ImageResponse
|
||||
from litellm.types.utils import ImageResponse, Usage
|
||||
|
||||
|
||||
def cost_calculator(
|
||||
@@ -39,11 +38,18 @@ def cost_calculator(
|
||||
)
|
||||
return 0.0
|
||||
|
||||
# Transform ImageUsage to Usage using the existing helper
|
||||
# ImageUsage has the same format as ResponseAPIUsage
|
||||
chat_usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(
|
||||
usage
|
||||
)
|
||||
# If usage is already a Usage object with completion_tokens_details set,
|
||||
# use it directly (it was already transformed in convert_to_image_response)
|
||||
if isinstance(usage, Usage) and usage.completion_tokens_details is not None:
|
||||
chat_usage = usage
|
||||
else:
|
||||
# Transform ImageUsage to Usage using the existing helper
|
||||
# ImageUsage has the same format as ResponseAPIUsage
|
||||
from litellm.responses.utils import ResponseAPILoggingUtils
|
||||
|
||||
chat_usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(
|
||||
usage
|
||||
)
|
||||
|
||||
# Use generic_cost_per_token for cost calculation
|
||||
prompt_cost, completion_cost = generic_cost_per_token(
|
||||
|
||||
@@ -19,10 +19,13 @@ import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.types.utils import (
|
||||
CompletionTokensDetailsWrapper,
|
||||
ImageResponse,
|
||||
ImageObject,
|
||||
ImageUsage,
|
||||
ImageUsageInputTokensDetails,
|
||||
PromptTokensDetailsWrapper,
|
||||
Usage,
|
||||
)
|
||||
|
||||
|
||||
@@ -202,6 +205,71 @@ class TestGPTImageCostRouting:
|
||||
assert cost >= 0
|
||||
|
||||
|
||||
class TestGPTImage15OutputImageTokens:
|
||||
"""
|
||||
Test for GitHub issue #19508:
|
||||
Image usage calculation does not include image tokens in gpt-image-1.5
|
||||
|
||||
gpt-image-1.5 returns output_tokens_details with separate image_tokens and text_tokens,
|
||||
and these must be correctly included in cost calculation.
|
||||
"""
|
||||
|
||||
def test_gpt_image_15_output_image_tokens_cost(self):
|
||||
"""
|
||||
Test that output image tokens are correctly included in cost calculation.
|
||||
|
||||
This tests the fix for issue #19508 where output_tokens_details.image_tokens
|
||||
were not being included in the cost calculation, causing costs to be
|
||||
underreported (e.g., $0.046 instead of $0.14).
|
||||
"""
|
||||
# Simulate gpt-image-1.5 response with output_tokens_details
|
||||
# This is what the API returns and what convert_to_image_response transforms
|
||||
usage = Usage(
|
||||
prompt_tokens=169,
|
||||
completion_tokens=4599,
|
||||
total_tokens=4768,
|
||||
prompt_tokens_details=PromptTokensDetailsWrapper(
|
||||
text_tokens=169,
|
||||
image_tokens=0,
|
||||
),
|
||||
completion_tokens_details=CompletionTokensDetailsWrapper(
|
||||
text_tokens=439,
|
||||
image_tokens=4160,
|
||||
),
|
||||
)
|
||||
|
||||
image_response = ImageResponse(
|
||||
created=1234567890,
|
||||
data=[ImageObject(b64_json="test")],
|
||||
)
|
||||
image_response.usage = usage
|
||||
image_response._hidden_params = {"custom_llm_provider": "openai"}
|
||||
|
||||
cost = litellm.completion_cost(
|
||||
completion_response=image_response,
|
||||
model="gpt-image-1.5",
|
||||
call_type="image_generation",
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
# gpt-image-1.5 pricing:
|
||||
# - input_cost_per_token: 5e-06 ($5/1M for text input)
|
||||
# - output_cost_per_token: 1e-05 ($10/1M for text output)
|
||||
# - output_cost_per_image_token: 3.2e-05 ($32/1M for image output)
|
||||
#
|
||||
# Expected cost:
|
||||
# Input text: 169 * $5/1M = $0.000845
|
||||
# Output text: 439 * $10/1M = $0.00439
|
||||
# Output image: 4160 * $32/1M = $0.13312
|
||||
# Total: $0.138355
|
||||
expected_cost = 169 * 5e-06 + 439 * 1e-05 + 4160 * 3.2e-05
|
||||
|
||||
assert abs(cost - expected_cost) < 1e-6, (
|
||||
f"Expected {expected_cost}, got {cost}. "
|
||||
f"Image tokens may not be included in cost calculation."
|
||||
)
|
||||
|
||||
|
||||
class TestCompletionCostIntegration:
|
||||
"""Test the full completion_cost integration for gpt-image-1"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user