mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(fal_ai): price nano-banana-2 and nano-banana-pro image generations by resolution (#43101)
* fix(fal_ai): price nano-banana-2 and nano-banana-pro image generations by resolution * fix(fal_ai): bill passthrough submits per requested image and register the resolution price keys --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
3d5660f32e
commit
cbba14682a
8 changed files with 219 additions and 7 deletions
|
|
@ -142,14 +142,35 @@ def _resolution_key(resolution: object) -> str | None:
|
|||
return str(resolution)
|
||||
|
||||
|
||||
def _resolution_cost_per_image(entry: Mapping[str, object] | None, resolution: object) -> float | None:
|
||||
resolution_key: Final = _resolution_key(resolution)
|
||||
if entry is None or resolution_key is None:
|
||||
return None
|
||||
cost: Final = entry.get(f"output_cost_per_image_{resolution_key}")
|
||||
return float(cost) if isinstance(cost, (int, float)) else None
|
||||
|
||||
|
||||
def _requested_image_count(request_body: Mapping[str, object]) -> int:
|
||||
num_images: Final = request_body.get("num_images")
|
||||
return num_images if type(num_images) is int and num_images > 0 else 1
|
||||
|
||||
|
||||
def _passthrough_cost_per_image(entry: Mapping[str, object], request_body: Mapping[str, object]) -> float | None:
|
||||
resolution_cost: Final = _resolution_cost_per_image(entry, request_body.get("resolution"))
|
||||
if resolution_cost is not None:
|
||||
return resolution_cost
|
||||
cost: Final = entry.get("output_cost_per_image")
|
||||
return float(cost) if isinstance(cost, (int, float)) else None
|
||||
|
||||
|
||||
def fal_ai_passthrough_cost(model: str, request_body: Mapping[str, object]) -> float | None:
|
||||
entry: Final = _entry(f"{litellm.LlmProviders.FAL_AI.value}/{model}")
|
||||
if entry is None:
|
||||
return None
|
||||
resolution: Final = _resolution_key(request_body.get("resolution"))
|
||||
keyed_cost: Final = entry.get(f"output_cost_per_image_{resolution}") if resolution is not None else None
|
||||
cost: Final = keyed_cost if isinstance(keyed_cost, (int, float)) else entry.get("output_cost_per_image")
|
||||
return float(cost) if isinstance(cost, (int, float)) else None
|
||||
cost_per_image: Final = _passthrough_cost_per_image(entry, request_body)
|
||||
if cost_per_image is None:
|
||||
return None
|
||||
return cost_per_image * _requested_image_count(request_body)
|
||||
|
||||
|
||||
def cost_calculator(
|
||||
|
|
@ -172,6 +193,11 @@ def cost_calculator(
|
|||
if deployment_cost_per_image is not None:
|
||||
return deployment_cost_per_image * len(images)
|
||||
params: Final[Mapping[str, object]] = optional_params or MappingProxyType({})
|
||||
resolution_cost_per_image: Final = _resolution_cost_per_image(
|
||||
_entry(f"{litellm.LlmProviders.FAL_AI.value}/{normalized_model}"), params.get("resolution")
|
||||
)
|
||||
if resolution_cost_per_image is not None:
|
||||
return resolution_cost_per_image * len(images)
|
||||
keyed_costs: Final = tuple(
|
||||
_keyed_cost_per_image(
|
||||
model=normalized_model,
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@ from .transformation import FalAIBaseConfig
|
|||
|
||||
class FalAINanoBananaConfig(FalAIBaseConfig):
|
||||
"""
|
||||
Configuration for Fal AI's Nano Banana / Gemini 2.5 Flash Image models.
|
||||
Configuration for Fal AI's Nano Banana family (Gemini Flash / Pro Image models).
|
||||
|
||||
Serves the imagen4 deprecation migration path. The same underlying model is
|
||||
exposed under two endpoints that share an identical schema:
|
||||
Serves the imagen4 deprecation migration path. Every endpoint shares the same
|
||||
request schema, so one config covers all of them:
|
||||
- fal-ai/nano-banana
|
||||
- fal-ai/gemini-25-flash-image
|
||||
- fal-ai/nano-banana-2
|
||||
- fal-ai/nano-banana-pro
|
||||
|
||||
Provider-specific params such as ``resolution`` ("0.5K", "1K", "2K", "4K") are
|
||||
forwarded as-is and drive the per-resolution price in the cost map.
|
||||
|
||||
Documentation: https://fal.ai/models/fal-ai/nano-banana
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -22852,6 +22852,37 @@
|
|||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/fal-ai/nano-banana-2": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "priced by the request's resolution field (0.5K, 1K default, 2K, 4K); the web search and high thinking surcharges are not modeled"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.08,
|
||||
"output_cost_per_image_0.5K": 0.06,
|
||||
"output_cost_per_image_1K": 0.08,
|
||||
"output_cost_per_image_2K": 0.12,
|
||||
"output_cost_per_image_4K": 0.16,
|
||||
"source": "https://fal.ai/models/fal-ai/nano-banana-2",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/fal-ai/nano-banana-pro": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "priced by the request's resolution field (1K default, 2K, 4K); the web search surcharge is not modeled"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.15,
|
||||
"output_cost_per_image_1K": 0.15,
|
||||
"output_cost_per_image_2K": 0.15,
|
||||
"output_cost_per_image_4K": 0.3,
|
||||
"source": "https://fal.ai/models/fal-ai/nano-banana-pro",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/openai/gpt-image-2": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
|
|
|
|||
|
|
@ -22852,6 +22852,37 @@
|
|||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/fal-ai/nano-banana-2": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "priced by the request's resolution field (0.5K, 1K default, 2K, 4K); the web search and high thinking surcharges are not modeled"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.08,
|
||||
"output_cost_per_image_0.5K": 0.06,
|
||||
"output_cost_per_image_1K": 0.08,
|
||||
"output_cost_per_image_2K": 0.12,
|
||||
"output_cost_per_image_4K": 0.16,
|
||||
"source": "https://fal.ai/models/fal-ai/nano-banana-2",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/fal-ai/nano-banana-pro": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
"comment": "priced by the request's resolution field (1K default, 2K, 4K); the web search surcharge is not modeled"
|
||||
},
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.15,
|
||||
"output_cost_per_image_1K": 0.15,
|
||||
"output_cost_per_image_2K": 0.15,
|
||||
"output_cost_per_image_4K": 0.3,
|
||||
"source": "https://fal.ai/models/fal-ai/nano-banana-pro",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"fal_ai/openai/gpt-image-2": {
|
||||
"litellm_provider": "fal_ai",
|
||||
"metadata": {
|
||||
|
|
|
|||
|
|
@ -624,6 +624,10 @@
|
|||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_image_0.5K": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_image_1024": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
|
|
@ -632,6 +636,18 @@
|
|||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_image_1K": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_image_2K": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_image_4K": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"output_cost_per_image_512": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from litellm.llms.fal_ai.image_generation import (
|
|||
get_fal_ai_image_generation_config,
|
||||
)
|
||||
from litellm.types.utils import ImageObject, ImageResponse
|
||||
from litellm.utils import get_optional_params_image_gen
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -23,6 +24,8 @@ from litellm.types.utils import ImageObject, ImageResponse
|
|||
"fal-ai/nano-banana",
|
||||
"nano-banana",
|
||||
"fal-ai/gemini-25-flash-image",
|
||||
"fal-ai/nano-banana-2",
|
||||
"fal-ai/nano-banana-pro",
|
||||
],
|
||||
)
|
||||
def test_nano_banana_config_selected(model):
|
||||
|
|
@ -145,3 +148,21 @@ def test_transform_request_includes_prompt_and_mapped_params():
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["fal-ai/nano-banana-2", "fal-ai/nano-banana-pro"])
|
||||
def test_resolution_extra_param_is_forwarded_to_fal(model):
|
||||
optional_params = get_optional_params_image_gen(
|
||||
model=model,
|
||||
n=1,
|
||||
size="1024x1024",
|
||||
custom_llm_provider="fal_ai",
|
||||
provider_config=FalAINanoBananaConfig(),
|
||||
resolution="4K",
|
||||
)
|
||||
request = FalAINanoBananaConfig().transform_image_generation_request(
|
||||
model=model,
|
||||
prompt="a cat",
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
assert request == {"prompt": "a cat", "num_images": 1, "aspect_ratio": "1:1", "resolution": "4K"}
|
||||
|
|
|
|||
|
|
@ -242,3 +242,77 @@ def test_passthrough_cost_is_none_only_when_no_price_applies_to_the_request(monk
|
|||
assert fal_ai_passthrough_cost("fal-ai/keyed-only-model", {}) is None
|
||||
assert fal_ai_passthrough_cost("fal-ai/keyed-only-model", {"resolution": 1024}) is None
|
||||
assert fal_ai_passthrough_cost("fal-ai/keyed-only-model", {"resolution": "512"}) == 0.02
|
||||
|
||||
|
||||
NANO_BANANA_RESOLUTION_MODELS: Final = ("fal-ai/nano-banana-2", "fal-ai/nano-banana-pro")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", NANO_BANANA_RESOLUTION_MODELS)
|
||||
def test_nano_banana_default_request_charges_the_1k_rate_per_image(model):
|
||||
entry: Final = litellm.model_cost[f"fal_ai/{model}"]
|
||||
cost: Final = cost_calculator(
|
||||
model=f"fal_ai/{model}",
|
||||
image_response=_image_response(num_images=2),
|
||||
optional_params={"num_images": 2, "aspect_ratio": "1:1"},
|
||||
)
|
||||
assert cost == 2 * entry["output_cost_per_image"] == 2 * entry["output_cost_per_image_1K"] > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", NANO_BANANA_RESOLUTION_MODELS)
|
||||
def test_nano_banana_4k_request_charges_the_4k_rate_above_1k(model):
|
||||
one_k: Final = cost_calculator(
|
||||
model=f"fal_ai/{model}", image_response=_image_response(), optional_params={"resolution": "1K"}
|
||||
)
|
||||
four_k: Final = cost_calculator(
|
||||
model=f"fal_ai/{model}", image_response=_image_response(), optional_params={"resolution": "4K"}
|
||||
)
|
||||
assert four_k == litellm.model_cost[f"fal_ai/{model}"]["output_cost_per_image_4K"]
|
||||
assert four_k > one_k > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", NANO_BANANA_RESOLUTION_MODELS)
|
||||
@pytest.mark.parametrize("resolution", ("1K", "2K", "4K"))
|
||||
def test_nano_banana_images_generations_and_passthrough_charge_the_same_tier(model, resolution):
|
||||
images_generations_cost: Final = cost_calculator(
|
||||
model=f"fal_ai/{model}", image_response=_image_response(), optional_params={"resolution": resolution}
|
||||
)
|
||||
assert images_generations_cost == fal_ai_passthrough_cost(model, {"resolution": resolution}) > 0
|
||||
|
||||
|
||||
def test_nano_banana_2_resolution_tiers_are_monotonic():
|
||||
costs: Final = tuple(
|
||||
cost_calculator(
|
||||
model="fal_ai/fal-ai/nano-banana-2",
|
||||
image_response=_image_response(),
|
||||
optional_params={"resolution": resolution},
|
||||
)
|
||||
for resolution in ("0.5K", "1K", "2K", "4K")
|
||||
)
|
||||
assert costs == tuple(sorted(costs)) and len(set(costs)) == len(costs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", NANO_BANANA_RESOLUTION_MODELS)
|
||||
def test_nano_banana_unpriced_resolution_falls_back_to_the_default_rate(model):
|
||||
cost: Final = cost_calculator(
|
||||
model=f"fal_ai/{model}", image_response=_image_response(), optional_params={"resolution": "8K"}
|
||||
)
|
||||
assert cost == litellm.model_cost[f"fal_ai/{model}"]["output_cost_per_image"] > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", NANO_BANANA_RESOLUTION_MODELS)
|
||||
def test_passthrough_num_images_multiplies_the_per_image_rate(model):
|
||||
entry: Final = litellm.model_cost[f"fal_ai/{model}"]
|
||||
assert fal_ai_passthrough_cost(model, {"num_images": 3}) == 3 * entry["output_cost_per_image"] > 0
|
||||
assert (
|
||||
fal_ai_passthrough_cost(model, {"resolution": "4K", "num_images": 2}) == 2 * entry["output_cost_per_image_4K"] > 0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_images", (None, 0, -2, True, 2.0, "2"))
|
||||
def test_passthrough_without_a_positive_integer_num_images_charges_one_image(num_images):
|
||||
body: Final = {} if num_images is None else {"num_images": num_images}
|
||||
assert (
|
||||
fal_ai_passthrough_cost("fal-ai/nano-banana-2", body)
|
||||
== litellm.model_cost["fal_ai/fal-ai/nano-banana-2"]["output_cost_per_image"]
|
||||
> 0
|
||||
)
|
||||
|
|
|
|||
|
|
@ -642,6 +642,10 @@ def validate_model_cost_values(model_data, exceptions=None):
|
|||
"output_cost_per_image_512",
|
||||
"output_cost_per_image_1024",
|
||||
"output_cost_per_image_1536",
|
||||
"output_cost_per_image_0.5K",
|
||||
"output_cost_per_image_1K",
|
||||
"output_cost_per_image_2K",
|
||||
"output_cost_per_image_4K",
|
||||
"input_cost_per_pixel",
|
||||
"output_cost_per_pixel",
|
||||
"input_cost_per_second",
|
||||
|
|
@ -875,6 +879,10 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
|
|||
"output_cost_per_image_512": {"type": "number"},
|
||||
"output_cost_per_image_1024": {"type": "number"},
|
||||
"output_cost_per_image_1536": {"type": "number"},
|
||||
"output_cost_per_image_0.5K": {"type": "number"},
|
||||
"output_cost_per_image_1K": {"type": "number"},
|
||||
"output_cost_per_image_2K": {"type": "number"},
|
||||
"output_cost_per_image_4K": {"type": "number"},
|
||||
"output_cost_per_image_token": {"type": "number"},
|
||||
"output_cost_per_video_token": {"type": "number"},
|
||||
"output_cost_per_pixel": {"type": "number"},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue