diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 9915d76433d..9f1f7df71a8 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -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 ### diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 2afb6ed4c58..304f790599d 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -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( diff --git a/litellm/llms/openai/image_generation/cost_calculator.py b/litellm/llms/openai/image_generation/cost_calculator.py index 350fdbaee2d..adb28d8199a 100644 --- a/litellm/llms/openai/image_generation/cost_calculator.py +++ b/litellm/llms/openai/image_generation/cost_calculator.py @@ -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 diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b3002fafc45..509a0c4a3ab 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -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", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b3002fafc45..509a0c4a3ab 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -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", diff --git a/tests/unit/test_image_cost_data_residency.py b/tests/unit/test_image_cost_data_residency.py new file mode 100644 index 00000000000..881c425e8ff --- /dev/null +++ b/tests/unit/test_image_cost_data_residency.py @@ -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)