mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-26 06:03:19 +00:00
fix(cost): apply the regional-processing uplift to gpt-image spend
completion_cost already received data_residency but never forwarded it into the image branch, so deployments with data_residency: eu never applied the +10% uplift to image-generation spend. The residency now flows through route_image_generation_cost_calculator into the OpenAI gpt-image calculator and its usage-based helpers, and the post-2026-03-05 gpt-image models carry the eu/us uplift multipliers like gpt-6 does.
This commit is contained in:
parent
b22a0bd9e5
commit
f594efe634
6 changed files with 194 additions and 15 deletions
|
|
@ -1660,6 +1660,7 @@ def completion_cost(
|
|||
call_type=call_type,
|
||||
model_info=_deployment_model_info(litellm_logging_obj, custom_pricing, router_model_id),
|
||||
vertex_location=vertex_location,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
elif call_type in _VIDEO_CALL_TYPES:
|
||||
### VIDEO GENERATION COST CALCULATION ###
|
||||
|
|
|
|||
|
|
@ -1805,6 +1805,7 @@ def calculate_image_response_cost_from_usage(
|
|||
custom_llm_provider: str,
|
||||
model_info: ModelInfo | None = None,
|
||||
vertex_location: str | None = None,
|
||||
data_residency: str | None = None,
|
||||
) -> float | None:
|
||||
"""
|
||||
Calculate image generation cost from usage metadata when available.
|
||||
|
|
@ -1889,6 +1890,7 @@ def calculate_image_response_cost_from_usage(
|
|||
custom_llm_provider=custom_llm_provider,
|
||||
model_info=model_info,
|
||||
vertex_location=vertex_location,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
return prompt_cost + completion_cost
|
||||
|
||||
|
|
@ -1953,6 +1955,7 @@ class CostCalculatorUtils:
|
|||
call_type: str | None = None,
|
||||
model_info: ModelInfo | None = None,
|
||||
vertex_location: str | None = None,
|
||||
data_residency: str | None = None,
|
||||
) -> float:
|
||||
"""
|
||||
Route the image generation cost calculator based on the custom_llm_provider
|
||||
|
|
@ -2104,6 +2107,7 @@ class CostCalculatorUtils:
|
|||
image_response=completion_response,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model_info=pricing,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
# Fall through to default for DALL-E models
|
||||
return default_image_cost_calculator(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ def cost_calculator(
|
|||
image_response: ImageResponse,
|
||||
custom_llm_provider: str | None = None,
|
||||
model_info: ModelInfo | None = None,
|
||||
data_residency: str | None = None,
|
||||
) -> float:
|
||||
"""Calculate cost for OpenAI gpt-image models (token-based pricing)."""
|
||||
provider: Final = custom_llm_provider or "openai"
|
||||
|
|
@ -38,7 +39,11 @@ def cost_calculator(
|
|||
# A chat Usage with an explicit output breakdown: cost via generic_cost_per_token.
|
||||
if isinstance(usage, Usage) and usage.completion_tokens_details is not None:
|
||||
prompt_cost, completion_cost = generic_cost_per_token(
|
||||
model=model, usage=usage, custom_llm_provider=provider, model_info=price_table
|
||||
model=model,
|
||||
usage=usage,
|
||||
custom_llm_provider=provider,
|
||||
model_info=price_table,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
return prompt_cost + completion_cost
|
||||
|
||||
|
|
@ -48,7 +53,11 @@ def cost_calculator(
|
|||
# does not itemize output and splitting text/image when it does.
|
||||
if getattr(usage, "input_tokens", None) is not None:
|
||||
token_based_cost: Final = calculate_image_response_cost_from_usage(
|
||||
model=model, image_response=image_response, custom_llm_provider=provider, model_info=price_table
|
||||
model=model,
|
||||
image_response=image_response,
|
||||
custom_llm_provider=provider,
|
||||
model_info=price_table,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
if token_based_cost is not None:
|
||||
return token_based_cost
|
||||
|
|
@ -57,7 +66,11 @@ def cost_calculator(
|
|||
# cost via generic_cost_per_token (text rate) instead of returning 0.0.
|
||||
if isinstance(usage, Usage):
|
||||
prompt_cost, completion_cost = generic_cost_per_token(
|
||||
model=model, usage=usage, custom_llm_provider=provider, model_info=price_table
|
||||
model=model,
|
||||
usage=usage,
|
||||
custom_llm_provider=provider,
|
||||
model_info=price_table,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
return prompt_cost + completion_cost
|
||||
|
||||
|
|
|
|||
|
|
@ -34044,7 +34044,9 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true
|
||||
"supports_pdf_input": true,
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2-2026-04-21": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34060,7 +34062,9 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true
|
||||
"supports_pdf_input": true,
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-flare": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34079,7 +34083,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-flare-2026-09-08": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34095,7 +34101,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-sunburst": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34114,7 +34122,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-sunburst-2026-09-08": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34130,7 +34140,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"low/1024-x-1024/gpt-image-1.5": {
|
||||
"deprecation_date": "2026-12-01",
|
||||
|
|
|
|||
|
|
@ -34044,7 +34044,9 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true
|
||||
"supports_pdf_input": true,
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2-2026-04-21": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34060,7 +34062,9 @@
|
|||
"/v1/images/edits"
|
||||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true
|
||||
"supports_pdf_input": true,
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-flare": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34079,7 +34083,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-flare-2026-09-08": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34095,7 +34101,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-sunburst": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34114,7 +34122,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"gpt-image-2.5-sunburst-2026-09-08": {
|
||||
"cache_read_input_image_token_cost": 2e-06,
|
||||
|
|
@ -34130,7 +34140,9 @@
|
|||
],
|
||||
"supports_vision": true,
|
||||
"supports_pdf_input": true,
|
||||
"source": "https://developers.openai.com/api/docs/pricing"
|
||||
"source": "https://developers.openai.com/api/docs/pricing",
|
||||
"regional_processing_uplift_multiplier_eu": 1.1,
|
||||
"regional_processing_uplift_multiplier_us": 1.1
|
||||
},
|
||||
"low/1024-x-1024/gpt-image-1.5": {
|
||||
"deprecation_date": "2026-12-01",
|
||||
|
|
|
|||
137
tests/unit/test_image_cost_data_residency.py
Normal file
137
tests/unit/test_image_cost_data_residency.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
Tests for the OpenAI regional-processing uplift on gpt-image spend
|
||||
|
||||
This tests the fix for GitHub issue #46407:
|
||||
https://github.com/BerriAI/litellm/issues/46407
|
||||
|
||||
`data_residency: eu` (routed through eu.api.openai.com) must apply the
|
||||
per-model regional uplift multiplier to image-generation spend exactly as it
|
||||
already does for chat/responses spend.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.types.utils import (
|
||||
ImageObject,
|
||||
ImageResponse,
|
||||
ImageUsage,
|
||||
ImageUsageInputTokensDetails,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _use_local_model_cost_map(monkeypatch):
|
||||
original_model_cost = litellm.model_cost
|
||||
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
litellm.get_model_info.cache_clear()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
litellm.model_cost = original_model_cost
|
||||
litellm.get_model_info.cache_clear()
|
||||
|
||||
|
||||
def _usage() -> ImageUsage:
|
||||
return ImageUsage(
|
||||
input_tokens=100,
|
||||
input_tokens_details=ImageUsageInputTokensDetails(image_tokens=50, text_tokens=50),
|
||||
output_tokens=200,
|
||||
total_tokens=300,
|
||||
)
|
||||
|
||||
|
||||
def _image_response() -> ImageResponse:
|
||||
return ImageResponse(
|
||||
created=1234567890,
|
||||
data=[ImageObject(url="http://example.com/image.jpg")],
|
||||
usage=_usage(),
|
||||
)
|
||||
|
||||
|
||||
class TestImageDataResidencyUplift:
|
||||
def test_image_usage_path_applies_eu_uplift(self) -> None:
|
||||
"""The ImageUsage path (calculate_image_response_cost_from_usage) must apply the EU multiplier."""
|
||||
from litellm.llms.openai.image_generation.cost_calculator import cost_calculator
|
||||
|
||||
image_response = _image_response()
|
||||
base_cost = cost_calculator(
|
||||
model="gpt-image-2.5-flare",
|
||||
image_response=image_response,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
eu_cost = cost_calculator(
|
||||
model="gpt-image-2.5-flare",
|
||||
image_response=image_response,
|
||||
custom_llm_provider="openai",
|
||||
data_residency="eu",
|
||||
)
|
||||
|
||||
assert base_cost > 0
|
||||
assert eu_cost == pytest.approx(base_cost * 1.1)
|
||||
|
||||
def test_route_from_completion_cost_applies_eu_uplift(self) -> None:
|
||||
"""completion_cost forwards data_residency into the image branch (issue #46407 part 1)."""
|
||||
base_cost = litellm.completion_cost(
|
||||
model="gpt-image-2.5-flare",
|
||||
custom_llm_provider="openai",
|
||||
completion_response=_image_response(),
|
||||
call_type="aimage_generation",
|
||||
)
|
||||
eu_cost = litellm.completion_cost(
|
||||
model="gpt-image-2.5-flare",
|
||||
custom_llm_provider="openai",
|
||||
completion_response=_image_response(),
|
||||
call_type="aimage_generation",
|
||||
data_residency="eu",
|
||||
)
|
||||
|
||||
assert base_cost > 0
|
||||
assert eu_cost == pytest.approx(base_cost * 1.1)
|
||||
|
||||
def test_us_uplift_applies_and_unknown_residency_does_not(self) -> None:
|
||||
from litellm.llms.openai.image_generation.cost_calculator import cost_calculator
|
||||
|
||||
image_response = _image_response()
|
||||
base_cost = cost_calculator(
|
||||
model="gpt-image-2.5-flare",
|
||||
image_response=image_response,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
us_cost = cost_calculator(
|
||||
model="gpt-image-2.5-flare",
|
||||
image_response=image_response,
|
||||
custom_llm_provider="openai",
|
||||
data_residency="us",
|
||||
)
|
||||
unknown_cost = cost_calculator(
|
||||
model="gpt-image-2.5-flare",
|
||||
image_response=image_response,
|
||||
custom_llm_provider="openai",
|
||||
data_residency="ap-southeast-1",
|
||||
)
|
||||
|
||||
assert us_cost == pytest.approx(base_cost * 1.1)
|
||||
assert unknown_cost == pytest.approx(base_cost)
|
||||
|
||||
def test_pre_cutoff_gpt_image_model_has_no_uplift(self) -> None:
|
||||
"""gpt-image-1.5 predates the 2026-03-05 cutoff and carries no multiplier: residency must not change spend."""
|
||||
from litellm.llms.openai.image_generation.cost_calculator import cost_calculator
|
||||
|
||||
image_response = _image_response()
|
||||
base_cost = cost_calculator(
|
||||
model="gpt-image-1.5",
|
||||
image_response=image_response,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
eu_cost = cost_calculator(
|
||||
model="gpt-image-1.5",
|
||||
image_response=image_response,
|
||||
custom_llm_provider="openai",
|
||||
data_residency="eu",
|
||||
)
|
||||
|
||||
assert base_cost > 0
|
||||
assert eu_cost == pytest.approx(base_cost)
|
||||
Loading…
Add table
Reference in a new issue