fix(cost): honor deployment pricing for image generation (#39311)

* fix image cost: honor deployment pricing

* fix types: coerce fal deployment price, drop private import

* fix: forward every custom pricing field through get_litellm_params

* test: assert optional keys are absent, not merely None, in get_litellm_params

* test: type the deployment image pricing test parameters

* fix: bill deployment per-image and per-pixel prices on unlisted image models

* test: type the remaining image cost test parameters

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
Meet Patel 2026-09-23 00:10:39 +05:30 • committed by GitHub
parent a788c4ab2b
commit 4bcdaf3d4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 518 additions and 61 deletions

View file

@ -1532,6 +1532,7 @@ def completion_cost(
size=size,
optional_params=optional_params,
call_type=call_type,
model_info=_deployment_model_info(litellm_logging_obj, custom_pricing, router_model_id),
)
elif call_type in _VIDEO_CALL_TYPES:
### VIDEO GENERATION COST CALCULATION ###
@ -2011,13 +2012,9 @@ def _deployment_model_info(
) -> ModelInfo | None:
if not custom_pricing:
return None
registered_deployment_info: Final = (
_cost_map_model_info(router_model_id, None)
if router_model_id is not None and router_model_id in litellm.model_cost
else None
)
registered_deployment_info: Final = _raw_cost_map_entry(router_model_id) if router_model_id is not None else None
if registered_deployment_info is not None:
return registered_deployment_info
return cast(ModelInfo, registered_deployment_info) # cast-ok: router registers deployment prices under its id
if litellm_logging_obj is None:
return None
litellm_params: Final = getattr(litellm_logging_obj, "litellm_params", None)
@ -2085,8 +2082,7 @@ def pricing_entry_for_cost_calc(
deployment_entry: Final = _deployment_model_info(litellm_logging_obj, custom_pricing, router_model_id)
deployment_key: Final = router_model_id or model
if deployment_entry is not None and deployment_key is not None:
registered_entry: Final = _raw_cost_map_entry(router_model_id) if router_model_id is not None else None
return deployment_key, registered_entry or deployment_entry
return deployment_key, deployment_entry
selected_model: Final = _select_model_name_for_cost_calc(
model=model,
completion_response=completion_response,
@ -2346,6 +2342,7 @@ def default_image_cost_calculator(
n: int | None = 1, # Default to 1 image
size: str | None = "1024-x-1024", # OpenAI default
optional_params: dict | None = None,
model_info: ModelInfo | None = None,
) -> float:
"""
Default image cost calculator for image generation
@ -2356,6 +2353,7 @@ def default_image_cost_calculator(
quality (Optional[str]): Image quality setting
n (Optional[int]): Number of images generated
size (Optional[str]): Image size (e.g. "1024x1024" or "1024-x-1024")
model_info (Optional[ModelInfo]): The deployment's own prices, consulted before the cost map
Returns:
float: Cost in USD for the image generation
@ -2386,9 +2384,7 @@ def default_image_cost_calculator(
model_without_provider: Final = f"{size_str}/{model.split('/')[-1]}"
model_with_quality_without_provider = f"{quality}/{model_without_provider}" if quality else model_without_provider
# Try model with quality first, fall back to base model name
cost_info: dict | None = None
models_to_check: Final[list[str | None]] = [
models_to_check: Final = (
model_name_with_quality,
base_model_name,
model_name_with_v2_quality,
@ -2396,22 +2392,33 @@ def default_image_cost_calculator(
model_without_provider,
model,
model_name_without_custom_llm_provider,
]
for _model in models_to_check:
if _model is not None and _model in litellm.model_cost:
cost_info = litellm.model_cost[_model]
break
if cost_info is None:
)
matched_model: Final = next(
(_model for _model in models_to_check if _model is not None and _model in litellm.model_cost), None
)
if matched_model is None and model_info is None:
raise Exception(f"Model not found in cost map. Tried checking {models_to_check}")
# Priority 1: Use per-image pricing if available (for gpt-image-1 and similar models)
if "input_cost_per_image" in cost_info and cost_info["input_cost_per_image"] is not None:
return cost_info["input_cost_per_image"] * n
# Priority 2: Fall back to per-pixel pricing for backward compatibility
elif "input_cost_per_pixel" in cost_info and cost_info["input_cost_per_pixel"] is not None:
return cost_info["input_cost_per_pixel"] * height * width * n
else:
shared_cost_info: Final = litellm.model_cost[matched_model] if matched_model is not None else None
price_tables: Final = tuple(table for table in (model_info, shared_cost_info) if table is not None)
image_count: Final = n if n is not None else 1
unit_counts: Final = (
("input_cost_per_image", image_count),
("output_cost_per_image", image_count),
("input_cost_per_pixel", height * width * image_count),
)
cost: Final = next(
(
price * units
for price_table in price_tables
for cost_key, units in unit_counts
if (price := price_table.get(cost_key)) is not None
),
None,
)
if cost is None:
raise Exception(f"No pricing information found for model {model}. Tried checking {models_to_check}")
return cost
def default_video_cost_calculator(

View file

@ -4,6 +4,7 @@ from typing import Final
from litellm.litellm_core_utils.core_helpers import normalize_drop_params
from litellm.llms.openai.data_residency import infer_openai_data_residency
from litellm.types.router import CustomPricingLiteLLMParams
AWS_CREDENTIAL_KWARGS_KEYS: Final = frozenset(
{
@ -65,6 +66,7 @@ OPTIONAL_KWARGS_KEYS: Final = (
}
)
| AWS_CREDENTIAL_KWARGS_KEYS
| frozenset(CustomPricingLiteLLMParams.model_fields)
)
# Backward-compatible alias for existing imports/tests.

View file

@ -24,6 +24,7 @@ from litellm.types.utils import (
CallTypes,
CompletionTokensDetailsWrapper,
CostPerToken,
CustomPricingLiteLLMParams,
DataResidency,
ImageResponse,
ModelInfo,
@ -49,6 +50,15 @@ _IMAGE_RESPONSE_CALL_TYPES: Final = frozenset(
# Pre-resolved DataResidency enum values for fast membership checks
_VALID_DATA_RESIDENCIES: Final = frozenset(r.value for r in DataResidency)
_DEPLOYMENT_PRICING_KEYS: Final[frozenset[str]] = frozenset(CustomPricingLiteLLMParams.model_fields)
_IMAGE_TOKEN_RATE_KEYS: Final[tuple[str, ...]] = (
"input_cost_per_token",
"output_cost_per_token",
"input_cost_per_image_token",
"output_cost_per_image_token",
)
# Pre-resolved service-tier cost-key suffixes (e.g. "_priority"). Used per
# request in the cost-calc path, so the f-strings are built once here instead
# of being rebuilt for every model_info key on every call. Longest-first so a
@ -826,6 +836,53 @@ def _get_cost_per_unit(model_info: ModelInfo, cost_key: str, default_value: floa
return default_value
def deployment_pricing(model_info: ModelInfo | None) -> ModelInfo | None:
"""The prices a deployment sets itself, as floats; None when it sets none that parse."""
if model_info is None:
return None
priced_keys: Final = tuple(key for key in _DEPLOYMENT_PRICING_KEYS if model_info.get(key) is not None)
pricing: Final = MappingProxyType(
{
key: price
for key in priced_keys
if (price := _get_cost_per_unit(model_info, key, default_value=None)) is not None
}
)
if not pricing:
return None
return cast(ModelInfo, pricing) # cast-ok: a read-only subset of ModelInfo pricing keys, values validated above
def prices_tokens(model_info: ModelInfo) -> bool:
"""Whether the price table carries any token rate, so a token-priced calculator can bill from usage."""
return any(model_info.get(key) is not None for key in _IMAGE_TOKEN_RATE_KEYS)
def flat_image_cost(model_info: ModelInfo | None, image_response: ImageResponse) -> float:
"""The per-image price times the images returned; 0.0 when the table sets no per-image price."""
if model_info is None:
return 0.0
output_cost_per_image: Final = _get_cost_per_unit(model_info, "output_cost_per_image", default_value=None) or 0.0
num_images: Final = len(image_response.data) if image_response.data else 0
return output_cost_per_image * num_images
def resolve_image_model_info(model: str, custom_llm_provider: str, model_info: ModelInfo | None) -> ModelInfo:
"""The price table an image cost calculator consults for ``model``.
``shared_backend_model_info`` keeps deployment prices off the shared ``{provider}/{model}`` key, so
a name lookup alone reads the public rate, and a model only the deployment prices has no entry at all.
"""
if model_info is None:
return get_model_info(model=model, custom_llm_provider=custom_llm_provider)
try:
shared_model_info: Final = get_model_info(model=model, custom_llm_provider=custom_llm_provider)
except Exception: # noqa: BLE001 # get_model_info raises a bare Exception for an unmapped model
return model_info
resolved: Final[ModelInfo] = {**shared_model_info, **model_info}
return resolved
def calculate_cache_writing_cost(
cache_creation_tokens: int,
cache_creation_token_details: CacheCreationTokenDetails | None,
@ -1711,6 +1768,7 @@ def calculate_image_response_cost_from_usage(
model: str,
image_response: ImageResponse,
custom_llm_provider: str,
model_info: ModelInfo | None = None,
) -> float | None:
"""
Calculate image generation cost from usage metadata when available.
@ -1735,6 +1793,9 @@ def calculate_image_response_cost_from_usage(
if prompt_tokens == 0 and completion_tokens == 0 and total_tokens == 0:
return None
if model_info is not None and not prices_tokens(model_info):
return None
input_tokens_details: Final[object] = getattr(usage, "input_tokens_details", None)
prompt_tokens_details: PromptTokensDetailsWrapper | None = None
if input_tokens_details is not None:
@ -1790,6 +1851,7 @@ def calculate_image_response_cost_from_usage(
model=model,
usage=normalized_usage,
custom_llm_provider=custom_llm_provider,
model_info=model_info,
)
return prompt_cost + completion_cost
@ -1850,9 +1912,15 @@ class CostCalculatorUtils:
size: str | None = None,
optional_params: dict | None = None,
call_type: str | None = None,
model_info: ModelInfo | None = None,
) -> float:
"""
Route the image generation cost calculator based on the custom_llm_provider
``model_info`` is the deployment's own price table. Its valid prices are laid over the shared
cost-map entry and handed to the provider calculator, so per-image, per-pixel and per-token
deployment prices all apply while provider logic (token-first billing, grounding surcharges,
image counting) stays in one place. An unparseable price is logged and ignored.
"""
from litellm.cost_calculator import default_image_cost_calculator
from litellm.llms.azure_ai.image_generation.cost_calculator import (
@ -1878,12 +1946,14 @@ class CostCalculatorUtils:
quality or completion_response.quality or _requested_image_param(optional_params, "quality") or "standard"
)
resolved_n: Final = n if n is not None else (len(completion_response.data) if completion_response.data else 0)
pricing: Final = deployment_pricing(model_info)
if custom_llm_provider == litellm.LlmProviders.VERTEX_AI.value:
if isinstance(completion_response, ImageResponse):
return vertex_ai_image_cost_calculator(
model=model,
image_response=completion_response,
model_info=pricing,
)
elif custom_llm_provider == litellm.LlmProviders.BEDROCK.value:
if isinstance(completion_response, ImageResponse):
@ -1902,6 +1972,7 @@ class CostCalculatorUtils:
return recraft_image_cost_calculator(
model=model,
image_response=completion_response,
model_info=pricing,
)
elif custom_llm_provider == litellm.LlmProviders.AIML.value:
from litellm.llms.aiml.image_generation.cost_calculator import (
@ -1911,6 +1982,7 @@ class CostCalculatorUtils:
return aiml_image_cost_calculator(
model=model,
image_response=completion_response,
model_info=pricing,
)
elif custom_llm_provider == litellm.LlmProviders.COMETAPI.value:
from litellm.llms.cometapi.image_generation.cost_calculator import (
@ -1920,6 +1992,7 @@ class CostCalculatorUtils:
return cometapi_image_cost_calculator(
model=model,
image_response=completion_response,
model_info=pricing,
)
elif custom_llm_provider == litellm.LlmProviders.GEMINI.value:
if call_type in (
@ -1933,6 +2006,7 @@ class CostCalculatorUtils:
return gemini_image_edit_cost_calculator(
model=model,
image_response=completion_response,
model_info=pricing,
)
from litellm.llms.gemini.image_generation.cost_calculator import (
cost_calculator as gemini_image_cost_calculator,
@ -1941,6 +2015,7 @@ class CostCalculatorUtils:
return gemini_image_cost_calculator(
model=model,
image_response=completion_response,
model_info=pricing,
)
elif custom_llm_provider == litellm.LlmProviders.AZURE_AI.value:
return azure_ai_image_cost_calculator(
@ -1949,6 +2024,7 @@ class CostCalculatorUtils:
size=resolved_size,
n=resolved_n,
optional_params=optional_params,
model_info=pricing,
)
elif custom_llm_provider == litellm.LlmProviders.FAL_AI.value:
from litellm.llms.fal_ai.cost_calculator import (
@ -1959,6 +2035,7 @@ class CostCalculatorUtils:
model=model,
image_response=completion_response,
optional_params=optional_params,
model_info=pricing,
)
elif custom_llm_provider == litellm.LlmProviders.RUNWAYML.value:
from litellm.llms.runwayml.cost_calculator import (
@ -1968,6 +2045,7 @@ class CostCalculatorUtils:
return runwayml_image_cost_calculator(
model=model,
image_response=completion_response,
model_info=pricing,
)
elif (
custom_llm_provider == litellm.LlmProviders.OPENAI.value
@ -1984,6 +2062,7 @@ class CostCalculatorUtils:
model=model,
image_response=completion_response,
custom_llm_provider=custom_llm_provider,
model_info=pricing,
)
# Fall through to default for DALL-E models
return default_image_cost_calculator(
@ -1993,6 +2072,7 @@ class CostCalculatorUtils:
n=resolved_n,
size=resolved_size,
optional_params=optional_params,
model_info=pricing,
)
else:
return default_image_cost_calculator(
@ -2002,5 +2082,6 @@ class CostCalculatorUtils:
n=resolved_n,
size=resolved_size,
optional_params=optional_params,
model_info=pricing,
)
return 0.0

View file

@ -1,19 +1,22 @@
from typing import Any, Final
import litellm
from litellm.types.utils import ImageResponse
from litellm.litellm_core_utils.llm_cost_calc.utils import resolve_image_model_info
from litellm.types.utils import ImageResponse, ModelInfo
def cost_calculator(
model: str,
image_response: Any,
model_info: ModelInfo | None = None,
) -> float:
"""
AI/ML flux image generation cost calculator
"""
_model_info: Final = litellm.get_model_info(
_model_info: Final = resolve_image_model_info(
model=model,
custom_llm_provider=litellm.LlmProviders.AIML.value,
model_info=model_info,
)
output_cost_per_image: Final[float] = _model_info.get("output_cost_per_image") or 0.0
num_images: int = 0

View file

@ -3,9 +3,22 @@ from typing import Any, Final
import litellm
from litellm.litellm_core_utils.llm_cost_calc.utils import (
_get_cost_per_unit,
calculate_image_response_cost_from_usage,
resolve_image_model_info,
)
from litellm.types.utils import ImageResponse
from litellm.types.utils import ImageResponse, ModelInfo
def _input_cost_per_pixel(resolved: ModelInfo) -> float:
deployment_price: Final = _get_cost_per_unit(resolved, "input_cost_per_pixel", default_value=None)
if deployment_price is not None:
return deployment_price
model_cost_key: Final = resolved.get("key")
shared_entry: Final = litellm.model_cost.get(model_cost_key) if model_cost_key is not None else None
if shared_entry is None:
return 0.0
return shared_entry.get("input_cost_per_pixel") or 0.0
def cost_calculator(
@ -14,13 +27,15 @@ def cost_calculator(
size: str | None = None,
n: int | None = None,
optional_params: Mapping[str, object] | None = None,
model_info: ModelInfo | None = None,
) -> float:
"""
Azure AI image generation cost calculator
"""
_model_info: Final = litellm.get_model_info(
_model_info: Final = resolve_image_model_info(
model=model,
custom_llm_provider=litellm.LlmProviders.AZURE_AI.value,
model_info=model_info,
)
if isinstance(image_response, ImageResponse):
@ -28,6 +43,7 @@ def cost_calculator(
model=model,
image_response=image_response,
custom_llm_provider=litellm.LlmProviders.AZURE_AI.value,
model_info=_model_info,
)
if token_based_cost is not None:
return token_based_cost
@ -37,9 +53,7 @@ def cost_calculator(
if output_cost_per_image:
return output_cost_per_image * num_images
model_cost: Final = litellm.model_cost[_model_info["key"]]
input_cost_per_pixel: Final[float] = model_cost.get("input_cost_per_pixel") or 0.0
if input_cost_per_pixel:
if _input_cost_per_pixel(_model_info):
from litellm.cost_calculator import default_image_cost_calculator
width: Final = optional_params.get("width") if optional_params else None
@ -50,10 +64,11 @@ def cost_calculator(
else size or image_response.size
)
return default_image_cost_calculator(
model=_model_info["key"],
model=_model_info.get("key", model),
custom_llm_provider=litellm.LlmProviders.AZURE_AI.value,
size=pixel_size,
n=num_images,
model_info=model_info,
)
return 0.0

View file

@ -1,19 +1,22 @@
from typing import Any, Final
import litellm
from litellm.types.utils import ImageResponse
from litellm.litellm_core_utils.llm_cost_calc.utils import resolve_image_model_info
from litellm.types.utils import ImageResponse, ModelInfo
def cost_calculator(
model: str,
image_response: Any,
model_info: ModelInfo | None = None,
) -> float:
"""
CometAPI image generation cost calculator
"""
_model_info: Final = litellm.get_model_info(
_model_info: Final = resolve_image_model_info(
model=model,
custom_llm_provider=litellm.LlmProviders.COMETAPI.value,
model_info=model_info,
)
output_cost_per_image: Final[float] = _model_info.get("output_cost_per_image") or 0.0
num_images: int = 0

View file

@ -7,7 +7,8 @@ from typing import Final
from pydantic import TypeAdapter
import litellm
from litellm.types.utils import ImageObject, ImageResponse
from litellm.litellm_core_utils.llm_cost_calc.utils import deployment_pricing, resolve_image_model_info
from litellm.types.utils import ImageObject, ImageResponse, ModelInfo
FAL_KEYED_PRICING_DEFAULT_QUALITY: Final[str] = "high"
_DEFAULT_KEYED_DIMENSIONS: Final[tuple[int, int]] = (1024, 768)
@ -149,6 +150,7 @@ def cost_calculator(
model: str,
image_response: object,
optional_params: Mapping[str, object] | None = None,
model_info: ModelInfo | None = None,
) -> float:
"""
fal.ai image generation cost calculator
@ -156,8 +158,14 @@ def cost_calculator(
if not isinstance(image_response, ImageResponse):
raise ValueError(f"image_response must be of type ImageResponse got type={type(image_response)}")
normalized_model: Final = model.removeprefix(f"{litellm.LlmProviders.FAL_AI.value}/")
params: Final[Mapping[str, object]] = optional_params or MappingProxyType({})
images: Final = tuple(image_response.data or ())
deployment_prices: Final = deployment_pricing(model_info)
deployment_cost_per_image: Final = (
None if deployment_prices is None else deployment_prices.get("output_cost_per_image")
)
if deployment_cost_per_image is not None:
return deployment_cost_per_image * len(images)
params: Final[Mapping[str, object]] = optional_params or MappingProxyType({})
keyed_costs: Final = tuple(
_keyed_cost_per_image(
model=normalized_model,
@ -168,15 +176,16 @@ def cost_calculator(
)
if not any(cost is None for cost in keyed_costs):
return sum(cost for cost in keyed_costs if cost is not None)
model_info: Final = litellm.get_model_info(
resolved_model_info: Final = resolve_image_model_info(
model=normalized_model,
custom_llm_provider=litellm.LlmProviders.FAL_AI.value,
model_info=deployment_prices,
)
raw_output_cost_per_image: Final = model_info.get("output_cost_per_image")
raw_output_cost_per_image: Final = resolved_model_info.get("output_cost_per_image")
output_cost_per_image: Final = (
float(raw_output_cost_per_image) if isinstance(raw_output_cost_per_image, (int, float)) else 0.0
)
raw_output_cost_per_pixel: Final = model_info.get("output_cost_per_pixel")
raw_output_cost_per_pixel: Final = resolved_model_info.get("output_cost_per_pixel")
output_cost_per_pixel: Final = (
float(raw_output_cost_per_pixel) if isinstance(raw_output_cost_per_pixel, (int, float)) else None
)

View file

@ -7,11 +7,13 @@ from typing import Any
from litellm.llms.gemini.image_generation.cost_calculator import (
cost_calculator as image_generation_cost_calculator,
)
from litellm.types.utils import ModelInfo
def cost_calculator(
model: str,
image_response: Any,
model_info: ModelInfo | None = None,
) -> float:
"""
Gemini image edit cost calculator.
@ -22,4 +24,5 @@ def cost_calculator(
return image_generation_cost_calculator(
model=model,
image_response=image_response,
model_info=model_info,
)

View file

@ -4,24 +4,26 @@ Google AI Image Generation Cost Calculator
from typing import Any, Final
import litellm
from litellm.litellm_core_utils.llm_cost_calc.utils import (
calculate_image_response_cost_from_usage,
calculate_image_response_web_search_cost,
resolve_image_model_info,
)
from litellm.types.utils import ImageResponse
from litellm.types.utils import ImageResponse, ModelInfo
def cost_calculator(
model: str,
image_response: Any,
model_info: ModelInfo | None = None,
) -> float:
"""
Google AI Image Generation Cost Calculator
"""
_model_info: Final = litellm.get_model_info(
_model_info: Final = resolve_image_model_info(
model=model,
custom_llm_provider="gemini",
model_info=model_info,
)
if not isinstance(image_response, ImageResponse):
@ -37,6 +39,7 @@ def cost_calculator(
model=model,
image_response=image_response,
custom_llm_provider="gemini",
model_info=_model_info,
)
if token_based_cost is not None:
return token_based_cost + web_search_cost

View file

@ -9,27 +9,37 @@ from typing import Final
from litellm import verbose_logger
from litellm.litellm_core_utils.llm_cost_calc.utils import (
calculate_image_response_cost_from_usage,
flat_image_cost,
generic_cost_per_token,
resolve_image_model_info,
)
from litellm.types.utils import ImageResponse, Usage
from litellm.types.utils import ImageResponse, ModelInfo, Usage
def cost_calculator(
model: str,
image_response: ImageResponse,
custom_llm_provider: str | None = None,
model_info: ModelInfo | None = None,
) -> float:
"""Calculate cost for OpenAI gpt-image models (token-based pricing)."""
provider: Final = custom_llm_provider or "openai"
price_table: Final = (
None
if model_info is None
else resolve_image_model_info(model=model, custom_llm_provider=provider, model_info=model_info)
)
usage: Final = getattr(image_response, "usage", None)
if usage is None:
verbose_logger.debug("No usage data available for %s, cannot calculate token-based cost", model)
return 0.0
provider: Final = custom_llm_provider or "openai"
return flat_image_cost(price_table, image_response)
# 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)
prompt_cost, completion_cost = generic_cost_per_token(
model=model, usage=usage, custom_llm_provider=provider, model_info=price_table
)
return prompt_cost + completion_cost
# ImageUsage / ResponseAPIUsage: reuse the shared helper (same path as
@ -38,7 +48,7 @@ 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=model, image_response=image_response, custom_llm_provider=provider, model_info=price_table
)
if token_based_cost is not None:
return token_based_cost
@ -46,7 +56,9 @@ def cost_calculator(
# Fallback: a Usage with no output breakdown that the image helper can't read —
# 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)
prompt_cost, completion_cost = generic_cost_per_token(
model=model, usage=usage, custom_llm_provider=provider, model_info=price_table
)
return prompt_cost + completion_cost
return 0.0
return flat_image_cost(price_table, image_response)

View file

@ -1,19 +1,22 @@
from typing import Any, Final
import litellm
from litellm.types.utils import ImageResponse
from litellm.litellm_core_utils.llm_cost_calc.utils import resolve_image_model_info
from litellm.types.utils import ImageResponse, ModelInfo
def cost_calculator(
model: str,
image_response: Any,
model_info: ModelInfo | None = None,
) -> float:
"""
Recraft image generation cost calculator
"""
_model_info: Final = litellm.get_model_info(
_model_info: Final = resolve_image_model_info(
model=model,
custom_llm_provider=litellm.LlmProviders.RECRAFT.value,
model_info=model_info,
)
output_cost_per_image: Final[float] = _model_info.get("output_cost_per_image") or 0.0
num_images: int = 0

View file

@ -1,12 +1,14 @@
from typing import Any, Final
import litellm
from litellm.types.utils import ImageResponse
from litellm.litellm_core_utils.llm_cost_calc.utils import resolve_image_model_info
from litellm.types.utils import ImageResponse, ModelInfo
def cost_calculator(
model: str,
image_response: Any,
model_info: ModelInfo | None = None,
) -> float:
"""
RunwayML image generation cost calculator.
@ -14,9 +16,10 @@ def cost_calculator(
RunwayML charges per image generated, not per pixel.
Pricing is stored in model_prices_and_context_window.json with output_cost_per_image.
"""
_model_info: Final = litellm.get_model_info(
_model_info: Final = resolve_image_model_info(
model=model,
custom_llm_provider=litellm.LlmProviders.RUNWAYML.value,
model_info=model_info,
)
output_cost_per_image: Final[float] = _model_info.get("output_cost_per_image") or 0.0
num_images: int = 0

View file

@ -4,24 +4,26 @@ Vertex AI Image Generation Cost Calculator
from typing import Final
import litellm
from litellm.litellm_core_utils.llm_cost_calc.utils import (
calculate_image_response_cost_from_usage,
calculate_image_response_web_search_cost,
resolve_image_model_info,
)
from litellm.types.utils import ImageResponse
from litellm.types.utils import ImageResponse, ModelInfo
def cost_calculator(
model: str,
image_response: ImageResponse,
model_info: ModelInfo | None = None,
) -> float:
"""
Vertex AI Image Generation Cost Calculator
"""
_model_info: Final = litellm.get_model_info(
_model_info: Final = resolve_image_model_info(
model=model,
custom_llm_provider="vertex_ai",
model_info=model_info,
)
web_search_cost: Final = calculate_image_response_web_search_cost(
@ -34,6 +36,7 @@ def cost_calculator(
model=model,
image_response=image_response,
custom_llm_provider="vertex_ai",
model_info=_model_info,
)
if token_based_cost is not None:
return token_based_cost + web_search_cost

View file

@ -1,6 +1,7 @@
import json
from collections.abc import Mapping
from datetime import datetime, timezone
from typing import cast
from typing import Final, cast
import pytest
@ -3637,6 +3638,138 @@ def test_get_token_base_cost_resolves_missing_cache_write_rates_like_the_tiered_
assert creation_1h == pytest.approx(expected_creation_1h)
def _image_response(num_images: int = 1, usage: ImageUsage | None = None) -> ImageResponse:
return ImageResponse(
data=[ImageObject(url="https://example.com/img.png") for _ in range(num_images)],
usage=usage,
)
_GPT_IMAGE_2_HIGH_1024: Final = {"quality": "high", "image_size": {"width": 1024, "height": 1024}}
@pytest.mark.parametrize(
("model", "optional_params", "model_info", "num_images", "expected_cost"),
[
("fal-ai/unlisted-image-model", None, {"output_cost_per_image": 0.08}, 1, 0.08),
("fal-ai/unlisted-image-model", None, {"output_cost_per_image": 0.08}, 2, 0.16),
("fal-ai/unlisted-image-model", None, {"output_cost_per_image": "0.08"}, 1, 0.08),
("openai/gpt-image-2", _GPT_IMAGE_2_HIGH_1024, {"output_cost_per_image": 0.5}, 1, 0.5),
("openai/gpt-image-2", _GPT_IMAGE_2_HIGH_1024, {"mode": "image_generation"}, 1, 0.211),
("openai/gpt-image-2", _GPT_IMAGE_2_HIGH_1024, {"output_cost_per_image": "0.08 USD"}, 1, 0.211),
],
)
def test_route_image_generation_cost_honors_deployment_model_info(
_local_model_cost_map: None,
model: str,
optional_params: dict[str, object] | None,
model_info: ModelInfo,
num_images: int,
expected_cost: float,
) -> None:
cost = CostCalculatorUtils.route_image_generation_cost_calculator(
model=model,
completion_response=_image_response(num_images),
custom_llm_provider="fal_ai",
optional_params=optional_params,
call_type="image_generation",
model_info=model_info,
)
assert cost == pytest.approx(expected_cost)
def test_route_image_generation_cost_openai_honors_deployment_input_cost_per_image(
_local_model_cost_map: None,
) -> None:
cost = CostCalculatorUtils.route_image_generation_cost_calculator(
model="dall-e-3",
completion_response=_image_response(),
custom_llm_provider="openai",
quality="standard",
size="1024-x-1024",
call_type="image_generation",
model_info={"input_cost_per_image": 0.07},
)
assert cost == pytest.approx(0.07)
def test_route_image_generation_cost_gemini_adds_grounding_to_deployment_image_price(
_local_model_cost_map: None,
) -> None:
usage = ImageUsage(
input_tokens=0,
input_tokens_details=ImageUsageInputTokensDetails(image_tokens=0, text_tokens=0),
output_tokens=0,
total_tokens=0,
web_search_requests=3,
)
cost = CostCalculatorUtils.route_image_generation_cost_calculator(
model="gemini/gemini-3.1-flash-image-preview",
completion_response=_image_response(usage=usage),
custom_llm_provider="gemini",
call_type="image_generation",
model_info={"output_cost_per_image": 0.1},
)
assert cost == pytest.approx(0.1 + 3 * 0.014)
def test_route_image_generation_cost_gemini_bills_tokens_when_no_image_returned(
_local_model_cost_map: None,
) -> None:
usage = ImageUsage(
input_tokens=10,
input_tokens_details=ImageUsageInputTokensDetails(image_tokens=0, text_tokens=10),
output_tokens=1290,
total_tokens=1300,
)
cost = CostCalculatorUtils.route_image_generation_cost_calculator(
model="gemini/gemini-3.1-flash-image-preview",
completion_response=ImageResponse(data=[], usage=usage),
custom_llm_provider="gemini",
call_type="image_generation",
model_info={"output_cost_per_image": 0.08},
)
assert cost == pytest.approx(10 * 5e-07 + 1290 * 6e-05)
@pytest.mark.parametrize(
("custom_llm_provider", "model"),
[
("gemini", "gemini/unlisted-image-model"),
("vertex_ai", "vertex_ai/unlisted-image-model"),
("azure_ai", "unlisted-image-model"),
("openai", "gpt-image-unlisted"),
],
)
def test_route_image_generation_cost_bills_deployment_image_price_when_unlisted_model_reports_tokens(
_local_model_cost_map: None,
custom_llm_provider: str,
model: str,
) -> None:
usage = ImageUsage(
input_tokens=10,
input_tokens_details=ImageUsageInputTokensDetails(image_tokens=0, text_tokens=10),
output_tokens=1290,
total_tokens=1300,
)
cost = CostCalculatorUtils.route_image_generation_cost_calculator(
model=model,
completion_response=_image_response(num_images=2, usage=usage),
custom_llm_provider=custom_llm_provider,
call_type="image_generation",
model_info={"output_cost_per_image": 0.05},
)
assert cost == pytest.approx(0.10)
def _batch_rates_model_info(**rates: object) -> ModelInfo:
return cast(ModelInfo, dict(rates))

View file

@ -4,6 +4,8 @@ Tests for get_litellm_params and related helpers.
Ensures backward compatibility after sparse kwargs extraction optimization.
"""
from typing import Final
import pytest
from litellm.litellm_core_utils.get_litellm_params import (
@ -12,6 +14,10 @@ from litellm.litellm_core_utils.get_litellm_params import (
get_litellm_params,
)
NAMED_PRICE_PARAMS: Final = frozenset(
{"input_cost_per_token", "output_cost_per_token", "input_cost_per_second", "output_cost_per_second"}
)
class TestGetBaseModelFromLitellmCallMetadata:
def test_none_metadata_returns_none(self):
@ -40,10 +46,27 @@ class TestGetLitellmParamsKwargsExtraction:
"""Verify that optional kwargs are correctly extracted via sparse extraction."""
def test_no_kwargs_omits_optional_keys(self):
"""When no kwargs passed, optional keys should not be in result."""
"""When no kwargs passed, optional keys are absent; the named price params are present as None."""
result = get_litellm_params(api_key="test-key")
for key in _OPTIONAL_KWARGS_KEYS:
for key in _OPTIONAL_KWARGS_KEYS - NAMED_PRICE_PARAMS:
assert key not in result
for key in NAMED_PRICE_PARAMS:
assert result[key] is None
def test_custom_pricing_kwargs_are_extracted(self) -> None:
from litellm.litellm_core_utils.litellm_logging import use_custom_pricing_for_model
from litellm.types.router import CustomPricingLiteLLMParams
assert set(CustomPricingLiteLLMParams.model_fields) <= _OPTIONAL_KWARGS_KEYS
result = get_litellm_params(output_cost_per_image=0.08, input_cost_per_audio_token=1e-6)
assert result["output_cost_per_image"] == 0.08
assert result["input_cost_per_audio_token"] == 1e-6
assert use_custom_pricing_for_model(result) is True
result_without_prices = get_litellm_params()
assert "output_cost_per_image" not in result_without_prices
assert use_custom_pricing_for_model(result_without_prices) is False
def test_present_kwargs_are_extracted(self):
result = get_litellm_params(

View file

@ -205,6 +205,36 @@ def test_flux2_flex_cost_accepts_lowercase_model_spelling():
assert cost == pytest.approx(5e-08 * 1536 * 1024 * 2)
def test_flux2_flex_cost_prefers_deployment_input_cost_per_pixel() -> None:
response: Final = ImageResponse(data=[ImageObject(b64_json="aW1n"), ImageObject(b64_json="aW1n")])
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="FLUX.2-flex",
completion_response=response,
custom_llm_provider="azure_ai",
size="2048x1024",
call_type="image_generation",
model_info={"input_cost_per_pixel": 2e-07},
)
assert cost == pytest.approx(2e-07 * 2048 * 1024 * 2)
def test_unlisted_azure_ai_model_bills_deployment_input_cost_per_pixel() -> None:
response: Final = ImageResponse(data=[ImageObject(b64_json="aW1n"), ImageObject(b64_json="aW1n")])
cost: Final = CostCalculatorUtils.route_image_generation_cost_calculator(
model="unlisted-flux-deployment",
completion_response=response,
custom_llm_provider="azure_ai",
size="1024x1024",
call_type="image_generation",
model_info={"input_cost_per_pixel": 1e-07},
)
assert cost == pytest.approx(1e-07 * 1024 * 1024 * 2)
def test_flux2_response_preserves_mapped_dimensions():
config = AzureFoundryFluxImageGenerationConfig()
params = config.map_openai_params(

View file

@ -1,6 +1,7 @@
import datetime
import time
from types import MappingProxyType
from pathlib import Path
from types import MappingProxyType, SimpleNamespace
from typing import Final, cast
import pytest
@ -22,8 +23,13 @@ from litellm.types.llms.base import CachedTokensDetails
from litellm.types.llms.openai import OpenAIRealtimeStreamList, ResponseAPIUsage, ResponsesAPIResponse
from litellm.types.rerank import RerankResponse
from litellm.types.utils import (
CacheCreationTokenDetails,
CallTypes,
Choices,
ImageObject,
ImageResponse,
ImageUsage,
ImageUsageInputTokensDetails,
LiteLLMRealtimeStreamLoggingObject,
Message,
ModelInfo,
@ -686,6 +692,90 @@ def test_tiered_pricing_only_deployment_selects_router_model_id():
assert router_model_id in selected
@pytest.mark.parametrize("metadata_key", ["metadata", "litellm_metadata"])
def test_completion_cost_image_generation_reads_deployment_model_info_price_from_logging_metadata(
_local_model_cost_map: None, metadata_key: str
) -> None:
cost = completion_cost(
completion_response=ImageResponse(data=[ImageObject(url="https://example.com/img.png")]),
model="fal_ai/fal-ai/unlisted-image-model",
call_type="image_generation",
custom_pricing=True,
litellm_logging_obj=SimpleNamespace(
litellm_params={metadata_key: {"model_info": {"output_cost_per_image": 0.08}}}
),
)
assert cost == pytest.approx(0.08)
def test_completion_cost_image_generation_registered_deployment_price_keeps_map_token_rates(
_local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
) -> None:
deployment_id: Final = "gemini-image-deployment-priced-per-image"
monkeypatch.setitem(
litellm.model_cost,
deployment_id,
{"mode": "image_generation", "litellm_provider": "gemini", "output_cost_per_image": 0.1},
)
usage: Final = ImageUsage(
input_tokens=10,
input_tokens_details=ImageUsageInputTokensDetails(image_tokens=0, text_tokens=10),
output_tokens=1290,
total_tokens=1300,
)
cost = completion_cost(
completion_response=ImageResponse(data=[ImageObject(url="https://example.com/img.png")], usage=usage),
model="gemini/gemini-3.1-flash-image-preview",
custom_llm_provider="gemini",
call_type="image_generation",
custom_pricing=True,
router_model_id=deployment_id,
litellm_logging_obj=SimpleNamespace(litellm_params={"metadata": {"model_info": {"id": deployment_id}}}),
)
assert cost == pytest.approx(10 * 5e-07 + 1290 * 6e-05)
def test_completion_cost_image_generation_ignores_deployment_model_info_without_custom_pricing(
_local_model_cost_map: None,
) -> None:
cost = completion_cost(
completion_response=ImageResponse(data=[ImageObject(url="https://example.com/img.png")]),
model="fal_ai/openai/gpt-image-2",
call_type="image_generation",
custom_pricing=False,
optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}},
litellm_logging_obj=SimpleNamespace(
litellm_params={"litellm_metadata": {"model_info": {"output_cost_per_image": 0.5}}}
),
)
assert cost == pytest.approx(0.211)
async def test_router_image_generation_bills_litellm_params_output_cost_per_image() -> None:
from litellm import Router
router = Router(
model_list=[
{
"model_name": "img",
"litellm_params": {
"model": "fal_ai/fal-ai/unlisted-image-model",
"api_key": "sk-fake",
"output_cost_per_image": 0.08,
},
}
]
)
response = await router.aimage_generation(model="img", prompt="x", mock_response="https://example.com/img.png")
assert response._hidden_params["response_cost"] == pytest.approx(0.08)
def test_tiered_pricing_only_deployment_completion_cost_is_nonzero():
"""End-to-end: a tier-only deployment must produce the tiered cost, not
$0. Mirrors the reported dashscope/qwen3.7-plus trace (12 prompt + 377

View file

@ -16,6 +16,8 @@ import litellm
from litellm.types.utils import (
ImageObject,
ImageResponse,
ImageUsage,
ImageUsageInputTokensDetails,
)
@ -52,6 +54,38 @@ class TestGPTImageCostCalculator:
assert cost == 0.0
@pytest.mark.parametrize(
"usage",
[
None,
ImageUsage(
input_tokens=0,
input_tokens_details=ImageUsageInputTokensDetails(image_tokens=0, text_tokens=0),
output_tokens=0,
total_tokens=0,
),
],
)
def test_gpt_image_1_bills_deployment_output_cost_per_image_without_usage_tokens(
self, usage: ImageUsage | None
) -> None:
from litellm.llms.openai.image_generation.cost_calculator import cost_calculator
image_response = ImageResponse(
created=1234567890,
data=[ImageObject(url="http://example.com/one.jpg"), ImageObject(url="http://example.com/two.jpg")],
usage=usage,
)
cost = cost_calculator(
model="gpt-image-1",
image_response=image_response,
custom_llm_provider="openai",
model_info={"output_cost_per_image": 0.05},
)
assert cost == pytest.approx(0.10)
class TestGPTImageCostRouting:
"""Test that gpt-image models are properly routed to the token-based calculator"""