feat: add resolution-based and per-megapixel pricing for FAL models

Enhance the FAL AI cost calculator to support three pricing modes:
- PER_MEGAPIXEL: cost scales with image dimensions (flux-2/klein models)
- PER_IMAGE_RESOLUTION: different rates for 2K vs 4K (nano-banana, gemini, seedream)
- PER_CALL: flat per-image rate (default, backward compatible)

Add pricing_basis, output_cost_per_megapixel, and output_cost_per_image_by_resolution
fields to ModelInfo. Update pricing JSON with correct USD values (1 credit = $0.01).
Set response size from request params in FAL transformation for cost calculation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Albert Sebastian 2026-04-14 04:31:12 +05:30
parent ea84e98c78
commit 926e2e641c
7 changed files with 416 additions and 27 deletions

View file

@ -968,6 +968,7 @@ class CostCalculatorUtils:
return fal_ai_image_cost_calculator(
model=model,
image_response=completion_response,
size=size,
)
elif custom_llm_provider == litellm.LlmProviders.RUNWAYML.value:
from litellm.llms.runwayml.cost_calculator import (

View file

@ -1,27 +1,85 @@
from typing import Any
from typing import Any, Optional
import litellm
from litellm.types.utils import ImageResponse
def _parse_megapixels(size: Optional[str]) -> Optional[float]:
"""Parse size string ('1024x1024' or '1024-x-1024') into megapixels."""
if not size:
return None
normalized = size.replace("-x-", "x").replace(" ", "")
parts = normalized.split("x")
if len(parts) != 2:
return None
try:
w, h = int(parts[0]), int(parts[1])
return (w * h) / 1_000_000
except (ValueError, TypeError):
return None
def _resolve_resolution_tier(size: Optional[str]) -> Optional[str]:
"""Map a size param to a resolution tier ('2K' or '4K').
Handles three cases:
1. Already a tier string: '2K', '4K' -> return as-is
2. Pixel dimensions: '1024x1024' -> compute MP -> classify
3. Unknown -> None (caller falls back to flat pricing)
Threshold: < 2.5 MP = '2K', >= 2.5 MP = '4K'
"""
if not size:
return None
if size.upper() in ("2K", "4K"):
return size.upper()
mp = _parse_megapixels(size)
if mp is None:
return None
return "4K" if mp >= 2.5 else "2K"
def cost_calculator(
model: str,
image_response: Any,
size: Optional[str] = None,
) -> float:
"""fal.ai image generation cost calculator.
Dispatches on pricing_basis from model info:
- PER_MEGAPIXEL: output_cost_per_megapixel * megapixels * num_images
- PER_IMAGE_RESOLUTION: output_cost_per_image_by_resolution[tier] * num_images
- PER_CALL / default: output_cost_per_image * num_images
"""
fal.ai image generation cost calculator
"""
if not isinstance(image_response, ImageResponse):
raise ValueError(
f"image_response must be of type ImageResponse, got type={type(image_response)}"
)
_model_info = litellm.get_model_info(
model=model,
custom_llm_provider=litellm.LlmProviders.FAL_AI.value,
)
num_images = len(image_response.data) if image_response.data else 0
effective_size = size or getattr(image_response, "size", None)
pricing_basis = _model_info.get("pricing_basis", "PER_CALL")
if pricing_basis == "PER_MEGAPIXEL":
cost_per_mp: float = _model_info.get("output_cost_per_megapixel") or 0.0
megapixels = _parse_megapixels(effective_size)
if cost_per_mp > 0 and megapixels and megapixels > 0:
return cost_per_mp * megapixels * num_images
# Fall through to flat if MP can't be computed
elif pricing_basis == "PER_IMAGE_RESOLUTION":
cost_by_res = _model_info.get("output_cost_per_image_by_resolution")
if cost_by_res:
tier = _resolve_resolution_tier(effective_size)
if tier and tier in cost_by_res:
return cost_by_res[tier] * num_images
# Fall through to flat if tier can't be resolved
# PER_CALL / fallback
output_cost_per_image: float = _model_info.get("output_cost_per_image") or 0.0
num_images: int = 0
if isinstance(image_response, ImageResponse):
if image_response.data:
num_images = len(image_response.data)
return output_cost_per_image * num_images
else:
raise ValueError(
f"image_response must be of type ImageResponse got type={type(image_response)}"
)
return output_cost_per_image * num_images

View file

@ -100,6 +100,10 @@ class FalAIBaseConfig(BaseImageGenerationConfig):
if not model_response.data:
model_response.data = []
# Preserve requested size on the response for cost calculation
if optional_params.get("size"):
model_response.size = optional_params["size"]
# Handle fal.ai response format
images = response_data.get("images", [])
if isinstance(images, list):

View file

@ -38206,5 +38206,255 @@
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_pdf_input": true
},
"fal_ai/bytedance/seedream/v4.5/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.03,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/bytedance/seedream/v4.5/text-to-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.03,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/bytedance/seedream/v4/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.03,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/bytedance/seedream/v4/text-to-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.03,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/bytedance/seedream/v5/lite/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/bytedance/seedream/v5/lite/text-to-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/flux-2/klein/9b": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_MEGAPIXEL",
"output_cost_per_megapixel": 0.01,
"output_cost_per_image": 0.01,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/flux-2/klein/9b/base/edit/lora": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_MEGAPIXEL",
"output_cost_per_megapixel": 0.01,
"output_cost_per_image": 0.01,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/flux-2/klein/9b/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_MEGAPIXEL",
"output_cost_per_megapixel": 0.01,
"output_cost_per_image": 0.01,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/flux-pro": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.04,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/flux-pro/kontext": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.05,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/gemini-25-flash-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/gemini-25-flash-image/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/gemini-3-pro-image-preview": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/gemini-3-pro-image-preview/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/gemini-3.1-flash-image-preview": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/gemini-3.1-flash-image-preview/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/iclight-v2": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/kling-image/omni": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.04,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/nano-banana-2": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/nano-banana-2/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/nano-banana-pro": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/nano-banana-pro/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
}
}

View file

@ -227,6 +227,9 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
] # only for vertex ai models
output_cost_per_image: Optional[float]
output_cost_per_image_token: Optional[float]
output_cost_per_megapixel: Optional[float] # for per-megapixel image pricing
output_cost_per_image_by_resolution: Optional[Dict[str, float]] # e.g. {"2K": 0.15, "4K": 0.30}
pricing_basis: Optional[str] # PER_CALL, PER_MEGAPIXEL, PER_IMAGE_RESOLUTION
output_vector_size: Optional[int]
output_cost_per_reasoning_token: Optional[float]
output_cost_per_video_per_second: Optional[float] # only for vertex ai models

View file

@ -5834,6 +5834,13 @@ def _get_model_info_helper( # noqa: PLR0915
output_cost_per_image_token=_model_info.get(
"output_cost_per_image_token", None
),
output_cost_per_megapixel=_model_info.get(
"output_cost_per_megapixel", None
),
output_cost_per_image_by_resolution=_model_info.get(
"output_cost_per_image_by_resolution", None
),
pricing_basis=_model_info.get("pricing_basis", None),
output_vector_size=_model_info.get("output_vector_size", None),
citation_cost_per_token=_model_info.get(
"citation_cost_per_token", None

View file

@ -12736,7 +12736,12 @@
"fal_ai/nano-banana-2": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.01,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12744,7 +12749,12 @@
"fal_ai/nano-banana-2/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.01,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12752,7 +12762,12 @@
"fal_ai/nano-banana-pro": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12760,7 +12775,12 @@
"fal_ai/nano-banana-pro/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12768,7 +12788,9 @@
"fal_ai/flux-2/klein/9b": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.025,
"pricing_basis": "PER_MEGAPIXEL",
"output_cost_per_megapixel": 0.01,
"output_cost_per_image": 0.01,
"supported_endpoints": [
"/v1/images/generations"
]
@ -12776,7 +12798,9 @@
"fal_ai/flux-2/klein/9b/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.025,
"pricing_basis": "PER_MEGAPIXEL",
"output_cost_per_megapixel": 0.01,
"output_cost_per_image": 0.01,
"supported_endpoints": [
"/v1/images/generations"
]
@ -12784,7 +12808,9 @@
"fal_ai/flux-2/klein/9b/base/edit/lora": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.025,
"pricing_basis": "PER_MEGAPIXEL",
"output_cost_per_megapixel": 0.01,
"output_cost_per_image": 0.01,
"supported_endpoints": [
"/v1/images/generations"
]
@ -12808,7 +12834,12 @@
"fal_ai/gemini-3-pro-image-preview": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.04,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12816,7 +12847,12 @@
"fal_ai/gemini-3-pro-image-preview/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.04,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12824,7 +12860,12 @@
"fal_ai/gemini-3.1-flash-image-preview": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12832,7 +12873,12 @@
"fal_ai/gemini-3.1-flash-image-preview/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12840,7 +12886,12 @@
"fal_ai/gemini-25-flash-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12848,7 +12899,12 @@
"fal_ai/gemini-25-flash-image/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12888,7 +12944,12 @@
"fal_ai/bytedance/seedream/v5/lite/text-to-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.03,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]
@ -12896,7 +12957,12 @@
"fal_ai/bytedance/seedream/v5/lite/edit": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.03,
"pricing_basis": "PER_IMAGE_RESOLUTION",
"output_cost_per_image": 0.15,
"output_cost_per_image_by_resolution": {
"2K": 0.15,
"4K": 0.3
},
"supported_endpoints": [
"/v1/images/generations"
]