mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
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:
parent
a788c4ab2b
commit
4bcdaf3d4b
18 changed files with 518 additions and 61 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue