Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion litellm/llms/fal_ai/image_generation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .flux_pro_v11_ultra_transformation import FalAIFluxProV11UltraConfig
from .flux_schnell_transformation import FalAIFluxSchnellConfig
from .imagen4_transformation import FalAIImagen4Config
from .nano_banana_transformation import FalAINanoBananaConfig
from .recraft_v3_transformation import FalAIRecraftV3Config
from .ideogram_v3_transformation import FalAIIdeogramV3Config
from .stable_diffusion_transformation import FalAIStableDiffusionConfig
Expand All @@ -20,6 +21,7 @@
"FalAIBaseConfig",
"FalAIImageGenerationConfig",
"FalAIImagen4Config",
"FalAINanoBananaConfig",
"FalAIRecraftV3Config",
"FalAIBriaConfig",
"FalAIFluxProV11Config",
Expand All @@ -45,7 +47,9 @@ def get_fal_ai_image_generation_config(model: str) -> BaseImageGenerationConfig:
model_lower = model.lower()

# Map model names to their corresponding configuration classes
if "imagen4" in model_lower or "imagen-4" in model_lower:
if "nano-banana" in model_lower or "gemini-25-flash-image" in model_lower:
return FalAINanoBananaConfig()
elif "imagen4" in model_lower or "imagen-4" in model_lower:
return FalAIImagen4Config()
elif "recraft" in model_lower:
return FalAIRecraftV3Config()
Expand Down
105 changes: 105 additions & 0 deletions litellm/llms/fal_ai/image_generation/nano_banana_transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from typing import List, Optional

from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams

from .transformation import FalAIBaseConfig


class FalAINanoBananaConfig(FalAIBaseConfig):
"""
Configuration for Fal AI's Nano Banana / Gemini 2.5 Flash Image models.

Serves the imagen4 deprecation migration path. The same underlying model is
exposed under two endpoints that share an identical schema:
- fal-ai/nano-banana
- fal-ai/gemini-25-flash-image

Documentation: https://fal.ai/models/fal-ai/nano-banana
"""

SUPPORTED_ASPECT_RATIOS: List[str] = [
"21:9",
"16:9",
"3:2",
"4:3",
"5:4",
"1:1",
"4:5",
"3:4",
"2:3",
"9:16",
]

def get_complete_url(
self,
api_base: Optional[str],
api_key: Optional[str],
model: str,
optional_params: dict,
litellm_params: dict,
stream: Optional[bool] = None,
) -> str:
base_url: str = (
api_base or get_secret_str("FAL_AI_API_BASE") or self.DEFAULT_BASE_URL
).rstrip("/")
endpoint = model if model.startswith("fal-ai/") else f"fal-ai/{model}"
return f"{base_url}/{endpoint}"

def get_supported_openai_params(
self, model: str
) -> List[OpenAIImageGenerationOptionalParams]:
return ["n", "response_format", "size"]

def map_openai_params(
self,
non_default_params: dict,
optional_params: dict,
model: str,
drop_params: bool,
) -> dict:
supported_params = self.get_supported_openai_params(model)
for key, value in non_default_params.items():
if key == "response_format":
continue
elif key == "n":
if "num_images" not in optional_params:
optional_params["num_images"] = value
elif key == "size":
if "aspect_ratio" not in optional_params:
optional_params["aspect_ratio"] = self._map_aspect_ratio(value)
elif key not in optional_params and not drop_params:
raise ValueError(
f"Parameter {key} is not supported for model {model}. "
f"Supported parameters are {supported_params}. "
"Set drop_params=True to drop unsupported parameters."
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return optional_params

def _map_aspect_ratio(self, size: str) -> str:
if not isinstance(size, str) or "x" not in size:
return "1:1"
try:
width, height = (int(part) for part in size.split("x"))
target = width / height
except (ValueError, ZeroDivisionError):
return "1:1"

def ratio_of(aspect_ratio: str) -> float:
w, h = (int(part) for part in aspect_ratio.split(":"))
return w / h

return min(
self.SUPPORTED_ASPECT_RATIOS,
key=lambda aspect_ratio: abs(ratio_of(aspect_ratio) - target),
)

def transform_image_generation_request(
self,
model: str,
prompt: str,
optional_params: dict,
litellm_params: dict,
headers: dict,
) -> dict:
return {"prompt": prompt, **optional_params}
16 changes: 16 additions & 0 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -13984,6 +13984,22 @@
"/v1/images/generations"
]
},
"fal_ai/fal-ai/nano-banana": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.039,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/gemini-25-flash-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.039,
"supported_endpoints": [
"/v1/images/generations"
]
},
"featherless_ai/featherless-ai/Qwerky-72B": {
"litellm_provider": "featherless_ai",
"max_input_tokens": 32768,
Expand Down
16 changes: 16 additions & 0 deletions model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -13984,6 +13984,22 @@
"/v1/images/generations"
]
},
"fal_ai/fal-ai/nano-banana": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.039,
"supported_endpoints": [
"/v1/images/generations"
]
},
"fal_ai/fal-ai/gemini-25-flash-image": {
"litellm_provider": "fal_ai",
"mode": "image_generation",
"output_cost_per_image": 0.039,
"supported_endpoints": [
"/v1/images/generations"
]
},
"featherless_ai/featherless-ai/Qwerky-72B": {
"litellm_provider": "featherless_ai",
"max_input_tokens": 32768,
Expand Down
5 changes: 5 additions & 0 deletions tests/image_gen_tests/test_fal_ai_image_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"fal_ai/fal-ai/stable-diffusion-v35-medium",
"fal-ai/stable-diffusion-v35-medium",
),
("fal_ai/fal-ai/nano-banana", "fal-ai/nano-banana"),
(
"fal_ai/fal-ai/gemini-25-flash-image",
"fal-ai/gemini-25-flash-image",
),
],
)
@pytest.mark.asyncio
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import os
import sys

import pytest

sys.path.insert(0, os.path.abspath("../../../../.."))

os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"

import litellm

litellm.model_cost = litellm.get_model_cost_map(url="")
from litellm.llms.fal_ai.cost_calculator import cost_calculator
from litellm.llms.fal_ai.image_generation import (
FalAIImagen4Config,
FalAINanoBananaConfig,
get_fal_ai_image_generation_config,
)
from litellm.types.utils import ImageObject, ImageResponse


@pytest.mark.parametrize(
"model",
[
"fal-ai/nano-banana",
"nano-banana",
"fal-ai/gemini-25-flash-image",
],
)
def test_nano_banana_config_selected(model):
assert isinstance(get_fal_ai_image_generation_config(model), FalAINanoBananaConfig)


def test_imagen4_still_routes_to_imagen4_config():
assert isinstance(
get_fal_ai_image_generation_config("fal-ai/imagen4/preview"),
FalAIImagen4Config,
)


@pytest.mark.parametrize(
"model,expected_url",
[
("fal-ai/nano-banana", "https://fal.run/fal-ai/nano-banana"),
(
"fal-ai/gemini-25-flash-image",
"https://fal.run/fal-ai/gemini-25-flash-image",
),
("nano-banana", "https://fal.run/fal-ai/nano-banana"),
],
)
def test_get_complete_url_derives_endpoint_from_model(model, expected_url):
url = FalAINanoBananaConfig().get_complete_url(
api_base=None,
api_key="test-key",
model=model,
optional_params={},
litellm_params={},
)
assert url == expected_url


def test_get_complete_url_respects_api_base_override():
url = FalAINanoBananaConfig().get_complete_url(
api_base="https://proxy.internal/",
api_key="test-key",
model="fal-ai/nano-banana",
optional_params={},
litellm_params={},
)
assert url == "https://proxy.internal/fal-ai/nano-banana"


def test_map_n_to_num_images():
optional_params = FalAINanoBananaConfig().map_openai_params(
non_default_params={"n": 3},
optional_params={},
model="fal-ai/nano-banana",
drop_params=False,
)
assert optional_params == {"num_images": 3}


@pytest.mark.parametrize(
"size,expected_aspect_ratio",
[
("1024x1024", "1:1"),
("512x512", "1:1"),
("1792x1024", "16:9"),
("1024x1792", "9:16"),
("1024x768", "4:3"),
("768x1024", "3:4"),
],
)
def test_map_size_to_aspect_ratio(size, expected_aspect_ratio):
optional_params = FalAINanoBananaConfig().map_openai_params(
non_default_params={"size": size},
optional_params={},
model="fal-ai/nano-banana",
drop_params=False,
)
assert optional_params == {"aspect_ratio": expected_aspect_ratio}


def test_response_format_is_ignored():
optional_params = FalAINanoBananaConfig().map_openai_params(
non_default_params={"response_format": "b64_json"},
optional_params={},
model="fal-ai/nano-banana",
drop_params=False,
)
assert optional_params == {}


def test_unsupported_param_raises_without_drop_params():
with pytest.raises(ValueError):
FalAINanoBananaConfig().map_openai_params(
non_default_params={"style": "vivid"},
optional_params={},
model="fal-ai/nano-banana",
drop_params=False,
)


def test_unsupported_param_dropped_with_drop_params():
optional_params = FalAINanoBananaConfig().map_openai_params(
non_default_params={"style": "vivid"},
optional_params={},
model="fal-ai/nano-banana",
drop_params=True,
)
assert optional_params == {}


def test_transform_request_includes_prompt_and_mapped_params():
request = FalAINanoBananaConfig().transform_image_generation_request(
model="fal-ai/nano-banana",
prompt="a cat",
optional_params={"num_images": 2, "aspect_ratio": "16:9"},
litellm_params={},
headers={},
)
assert request == {
"prompt": "a cat",
"num_images": 2,
"aspect_ratio": "16:9",
}


@pytest.mark.parametrize(
"model", ["fal-ai/nano-banana", "fal-ai/gemini-25-flash-image"]
)
def test_nano_banana_pricing_registered(model):
info = litellm.get_model_info(
model=model, custom_llm_provider=litellm.LlmProviders.FAL_AI.value
)
assert info["output_cost_per_image"] == 0.039
assert info["mode"] == "image_generation"


def test_cost_calculator_scales_with_image_count():
image_response = ImageResponse(
data=[ImageObject(url="https://x/1.png"), ImageObject(url="https://x/2.png")]
)
cost = cost_calculator(model="fal-ai/nano-banana", image_response=image_response)
assert cost == pytest.approx(0.078)
Loading