mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(images): support GPT Image 2.5 quality and preserve JSON edits
This commit is contained in:
parent
afa0b23dc2
commit
eea527e9b1
10 changed files with 168 additions and 20 deletions
|
|
@ -869,6 +869,8 @@ def image_edit(
|
|||
non_default_params,
|
||||
extra_body if isinstance(extra_body, dict) else None,
|
||||
)
|
||||
if image_edit_provider_config.use_multipart_form_data()
|
||||
else {**non_default_params, **(extra_body if isinstance(extra_body, dict) else {})}
|
||||
)
|
||||
|
||||
# Pre Call logging
|
||||
|
|
|
|||
|
|
@ -1645,7 +1645,31 @@ def calculate_image_response_cost_from_usage(
|
|||
usage=normalized_usage,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
return prompt_cost + completion_cost
|
||||
cached_details: Final = (
|
||||
input_tokens_details.get("cached_tokens_details")
|
||||
if isinstance(input_tokens_details, dict)
|
||||
else getattr(input_tokens_details, "cached_tokens_details", None)
|
||||
)
|
||||
if cached_details is None:
|
||||
return prompt_cost + completion_cost
|
||||
model_info: Final = get_model_info(model=model, custom_llm_provider=custom_llm_provider)
|
||||
cached_text: Final = _get_token_detail_value(cached_details, "text_tokens") or 0
|
||||
cached_image: Final = _get_token_detail_value(cached_details, "image_tokens") or 0
|
||||
input_text_tokens: Final = _get_token_detail_value(input_tokens_details, "text_tokens") or 0
|
||||
input_image_tokens: Final = _get_token_detail_value(input_tokens_details, "image_tokens") or 0
|
||||
if not (0 <= cached_text <= input_text_tokens and 0 <= cached_image <= input_image_tokens):
|
||||
raise ValueError("Image cached token counts exceed their input modality counts")
|
||||
text_rate: Final = model_info.get("input_cost_per_token") or 0.0
|
||||
image_rate: Final = model_info.get("input_cost_per_image_token")
|
||||
cache_text_rate: Final = model_info.get("cache_read_input_token_cost")
|
||||
cache_image_rate: Final = model_info.get("cache_read_input_image_token_cost")
|
||||
text_savings: Final = cached_text * (text_rate - cache_text_rate) if cache_text_rate is not None else 0.0
|
||||
image_savings: Final = (
|
||||
cached_image * ((image_rate if image_rate is not None else text_rate) - cache_image_rate)
|
||||
if cache_image_rate is not None
|
||||
else 0.0
|
||||
)
|
||||
return prompt_cost + completion_cost - text_savings - image_savings
|
||||
|
||||
|
||||
def calculate_image_response_web_search_cost(
|
||||
|
|
|
|||
|
|
@ -29797,6 +29797,7 @@
|
|||
},
|
||||
"gpt-image-2.5-flare": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29807,11 +29808,11 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-flare"
|
||||
},
|
||||
"gpt-image-2.5-flare-2026-09-08": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29822,11 +29823,11 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-flare"
|
||||
},
|
||||
"gpt-image-2.5-sunburst": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29837,11 +29838,11 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst"
|
||||
},
|
||||
"gpt-image-2.5-sunburst-2026-09-08": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29852,8 +29853,7 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst"
|
||||
},
|
||||
"low/1024-x-1024/gpt-image-1.5": {
|
||||
"deprecation_date": "2026-12-01",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ImageEditOptionalRequestParams(TypedDict, total=False):
|
|||
input_fidelity: Literal["high", "low"] | None
|
||||
mask: str | None
|
||||
n: int | None
|
||||
quality: Literal["high", "medium", "low", "standard", "auto"] | None
|
||||
quality: Literal["high", "medium", "low", "standard", "auto", "xhigh", "max"] | None
|
||||
response_format: Literal["url", "b64_json"] | None
|
||||
size: str | None
|
||||
user: str | None
|
||||
|
|
|
|||
|
|
@ -2273,6 +2273,8 @@ class ImageGenerationRequestQuality(str, Enum):
|
|||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
XHIGH = "xhigh"
|
||||
MAX = "max"
|
||||
AUTO = "auto"
|
||||
STANDARD = "standard"
|
||||
HD = "hd"
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
|
|||
cache_creation_input_token_cost_priority: float | None # OpenAI priority service tier pricing
|
||||
cache_creation_input_token_cost_ultrafast: ReadOnly[float | None] # OpenAI ultrafast service tier pricing
|
||||
cache_read_input_token_cost: float | None
|
||||
cache_read_input_image_token_cost: ReadOnly[float | None]
|
||||
cache_read_input_token_cost_flex: float | None # OpenAI flex service tier pricing
|
||||
cache_read_input_token_cost_priority: float | None # OpenAI priority service tier pricing
|
||||
cache_read_input_token_cost_ultrafast: ReadOnly[float | None] # OpenAI ultrafast service tier pricing
|
||||
|
|
@ -3804,6 +3805,9 @@ all_litellm_params = (
|
|||
"enable_tag_filtering",
|
||||
"enable_json_schema_validation",
|
||||
"use_xai_oauth",
|
||||
"chatgpt_auth_profile",
|
||||
"chatgpt_token_dir",
|
||||
"chatgpt_auth_file",
|
||||
"auto_router_config_path",
|
||||
"auto_router_config",
|
||||
"auto_router_default_model",
|
||||
|
|
|
|||
|
|
@ -5897,6 +5897,7 @@ def _get_model_info_helper(
|
|||
input_cost_per_second=_model_info.get("input_cost_per_second", None),
|
||||
input_cost_per_audio_token=_model_info.get("input_cost_per_audio_token", None),
|
||||
input_cost_per_image_token=_model_info.get("input_cost_per_image_token", None),
|
||||
cache_read_input_image_token_cost=_model_info.get("cache_read_input_image_token_cost", None),
|
||||
input_cost_per_video_token=_model_info.get("input_cost_per_video_token", None),
|
||||
input_cost_per_image=_model_info.get("input_cost_per_image", None),
|
||||
input_cost_per_audio_per_second=_model_info.get("input_cost_per_audio_per_second", None),
|
||||
|
|
|
|||
|
|
@ -29797,6 +29797,7 @@
|
|||
},
|
||||
"gpt-image-2.5-flare": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29807,11 +29808,11 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-flare"
|
||||
},
|
||||
"gpt-image-2.5-flare-2026-09-08": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29822,11 +29823,11 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-flare"
|
||||
},
|
||||
"gpt-image-2.5-sunburst": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29837,11 +29838,11 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst"
|
||||
},
|
||||
"gpt-image-2.5-sunburst-2026-09-08": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
|
|
@ -29852,8 +29853,7 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst"
|
||||
},
|
||||
"low/1024-x-1024/gpt-image-1.5": {
|
||||
"deprecation_date": "2026-12-01",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import base64
|
||||
import json
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
|
@ -6,9 +8,87 @@ import pytest
|
|||
import litellm
|
||||
from litellm.llms.chatgpt.images import ChatGPTImageEditConfig, ChatGPTImageGenerationConfig
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
from litellm.types.llms.openai import ImageGenerationRequestQuality
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,quality",
|
||||
[
|
||||
("gpt-image-2", ImageGenerationRequestQuality.AUTO),
|
||||
("gpt-image-2.5-flare", ImageGenerationRequestQuality.XHIGH),
|
||||
("gpt-image-2.5-flare", ImageGenerationRequestQuality.MAX),
|
||||
("gpt-image-2.5-sunburst", ImageGenerationRequestQuality.XHIGH),
|
||||
("gpt-image-2.5-sunburst", ImageGenerationRequestQuality.MAX),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("editing", [False, True])
|
||||
def test_image_25_transmits_model_quality_and_transparency(model, quality, editing, chatgpt_tokens):
|
||||
expected: Final = {
|
||||
"model": model,
|
||||
"prompt": "a red circle with transparent surroundings",
|
||||
"quality": quality.value,
|
||||
"background": "transparent",
|
||||
"size": "2048x2048",
|
||||
**({"images": [{"image_url": "data:image/png;base64,aGVsbG8="}]} if editing else {}),
|
||||
}
|
||||
|
||||
def respond(request):
|
||||
assert str(request.url) == "https://chatgpt.com/backend-api/codex/images/" + (
|
||||
"edits" if editing else "generations"
|
||||
)
|
||||
assert request.headers["content-type"] == "application/json"
|
||||
assert json.loads(request.content) == expected
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={"created": 1, "data": [{"b64_json": "aGVsbG8="}], "quality": quality.value},
|
||||
)
|
||||
|
||||
client: Final = HTTPHandler()
|
||||
client.client = httpx.Client(transport=httpx.MockTransport(respond))
|
||||
operation: Final = litellm.image_edit if editing else litellm.image_generation
|
||||
try:
|
||||
response: Final = operation(
|
||||
**{**expected, "model": "chatgpt/" + model, "quality": quality},
|
||||
client=client,
|
||||
chatgpt_token_dir=chatgpt_tokens,
|
||||
)
|
||||
assert response.data[0].b64_json == "aGVsbG8="
|
||||
assert response.quality == quality.value
|
||||
finally:
|
||||
client.client.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["gpt-image-2", "gpt-image-2.5-flare", "gpt-image-2.5-sunburst"])
|
||||
def test_json_edit_preserves_provider_params_and_extra_body_precedence(model, chatgpt_tokens):
|
||||
references: Final = [{"image_url": "data:image/png;base64,aGVsbG8="}]
|
||||
|
||||
def respond(request):
|
||||
assert request.headers["content-type"] == "application/json"
|
||||
assert json.loads(request.content) == {
|
||||
"model": model,
|
||||
"prompt": "red circle",
|
||||
"images": references,
|
||||
"seed": 7,
|
||||
"provider_options": {"steps": 30, "enabled": True},
|
||||
"output_compression": 90,
|
||||
}
|
||||
return httpx.Response(200, json={"created": 1, "data": [{"b64_json": "aGVsbG8="}]})
|
||||
|
||||
with httpx.Client(transport=httpx.MockTransport(respond)) as http_client:
|
||||
response: Final = litellm.image_edit(
|
||||
model="chatgpt/" + model,
|
||||
prompt="red circle",
|
||||
images=references,
|
||||
client=HTTPHandler(client=http_client),
|
||||
chatgpt_token_dir=chatgpt_tokens,
|
||||
seed=42,
|
||||
output_compression=90,
|
||||
extra_body={"seed": 7, "provider_options": {"steps": 30, "enabled": True}},
|
||||
)
|
||||
assert response.data[0].b64_json == "aGVsbG8="
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_base", [None, "https://image-gateway.test"])
|
||||
def test_generation_routes_with_chatgpt_oauth(chatgpt_tokens, api_base):
|
||||
requests = []
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ gpt-image-1 uses token-based pricing:
|
|||
- Image Output: $40.00/1M tokens
|
||||
"""
|
||||
|
||||
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.types.utils import (
|
||||
CompletionTokensDetailsWrapper,
|
||||
ImageResponse,
|
||||
ImageObject,
|
||||
ImageResponse,
|
||||
ImageUsage,
|
||||
ImageUsageInputTokensDetails,
|
||||
PromptTokensDetailsWrapper,
|
||||
|
|
@ -42,6 +42,41 @@ def _use_local_model_cost_map(monkeypatch):
|
|||
class TestGPTImageCostCalculator:
|
||||
"""Test the OpenAI gpt-image cost calculator"""
|
||||
|
||||
@pytest.mark.parametrize("family", ["flare", "sunburst"])
|
||||
@pytest.mark.parametrize("snapshot", ["", "-2026-09-08"])
|
||||
@pytest.mark.parametrize("call_type", ["image_generation", "image_edit"])
|
||||
@pytest.mark.parametrize("cached_text,cached_image", [(0, 0), (50, 500)])
|
||||
def test_image_25_official_prices(self, family, snapshot, call_type, cached_text, cached_image):
|
||||
response: Final = ImageResponse(
|
||||
created=1,
|
||||
data=[],
|
||||
usage={
|
||||
"input_tokens": 1100,
|
||||
"output_tokens": 100,
|
||||
"total_tokens": 1200,
|
||||
"input_tokens_details": {
|
||||
"text_tokens": 100,
|
||||
"image_tokens": 1000,
|
||||
"cached_tokens": cached_text + cached_image,
|
||||
"cached_tokens_details": {"text_tokens": cached_text, "image_tokens": cached_image},
|
||||
},
|
||||
},
|
||||
)
|
||||
cost: Final = litellm.completion_cost(
|
||||
model="gpt-image-2.5-" + family + snapshot,
|
||||
completion_response=response,
|
||||
call_type=call_type,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
expected: Final = (
|
||||
(100 - cached_text) * 5e-6
|
||||
+ cached_text * 1.25e-6
|
||||
+ (1000 - cached_image) * 8e-6
|
||||
+ cached_image * 2e-6
|
||||
+ 100 * 30e-6
|
||||
)
|
||||
assert cost == pytest.approx(expected)
|
||||
|
||||
def test_gpt_image_1_cost_with_text_only(self):
|
||||
"""Test cost calculation with only text input tokens"""
|
||||
from litellm.llms.openai.image_generation.cost_calculator import cost_calculator
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue