feat(fal_ai): add gpt-image-2.5 flare/sunburst, flux/dev and image edits

Route openai/gpt-image-2.5/{flare,sunburst}/text-to-image through the existing GPT Image config with the xhigh and max quality tiers, add a dedicated fal-ai/flux/dev config, and add a Fal image-edit config so /v1/images/edits works for the gpt-image-2.5 and gpt-image-2 edit endpoints. Add flat and quality-by-size keyed pricing rows so spend is non-zero

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kerry 2026-09-20 05:14:15 +00:00
parent dd40137463
commit 365dc9a3b5
14 changed files with 3159 additions and 46 deletions

View file

@ -19,10 +19,10 @@ FAL_NAMED_IMAGE_SIZES: Final[Mapping[str, str]] = MappingProxyType(
)
def _keyed_size(model: str, optional_params: Mapping[str, object]) -> str | None:
def _keyed_size(optional_params: Mapping[str, object]) -> str | None:
image_size: Final = optional_params.get("image_size")
if image_size is None:
return None if model.endswith("/edit") else FAL_TEXT_TO_IMAGE_DEFAULT_SIZE
if image_size is None or image_size == "auto":
return FAL_TEXT_TO_IMAGE_DEFAULT_SIZE
if isinstance(image_size, Mapping):
width: Final = image_size.get("width")
height: Final = image_size.get("height")
@ -37,7 +37,7 @@ def _keyed_size(model: str, optional_params: Mapping[str, object]) -> str | None
def _keyed_cost_per_image(model: str, optional_params: Mapping[str, object] | None) -> float | None:
if optional_params is None:
return None
size: Final = _keyed_size(model=model, optional_params=optional_params)
size: Final = _keyed_size(optional_params)
if size is None:
return None
raw_quality: Final = optional_params.get("quality")

View file

@ -0,0 +1,3 @@
from .transformation import FalAIImageEditConfig
__all__ = ("FalAIImageEditConfig",)

View file

@ -0,0 +1,165 @@
import base64
from collections.abc import Mapping
from io import BufferedReader, BytesIO
from types import MappingProxyType
from typing import TYPE_CHECKING, Final
import httpx
from httpx._types import RequestFiles
from litellm.images.utils import ImageEditRequestUtils
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.fal_ai.image_generation.gpt_image_2_transformation import (
map_gpt_image_quality,
map_gpt_image_size,
)
from litellm.llms.fal_ai.image_generation.transformation import fal_images_to_image_objects
from litellm.secret_managers.main import get_secret_str
from litellm.types.images.main import ImageEditOptionalRequestParams
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import FileTypes, ImageResponse
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
DEFAULT_BASE_URL: Final[str] = "https://fal.run"
EDIT_SUFFIX: Final[str] = "/edit"
SUPPORTED_OPENAI_PARAMS: Final[tuple[str, ...]] = ("background", "mask", "n", "quality", "size")
PARAM_TRANSLATION: Final[Mapping[str, str]] = MappingProxyType(
{
"background": "background",
"n": "num_images",
"quality": "quality",
"size": "image_size",
}
)
def _read_image_bytes(image: object) -> bytes:
if isinstance(image, bytes):
return image
if isinstance(image, (BytesIO, BufferedReader)):
position: Final = image.tell()
image.seek(0)
data: Final = image.read()
image.seek(position)
return data
raise ValueError(f"Unsupported image type for Fal AI image edit: {type(image).__name__}")
def _to_data_url(image: object) -> str:
if isinstance(image, str):
return image
image_bytes: Final = _read_image_bytes(image)
mime_type: Final = ImageEditRequestUtils.get_image_content_type(image_bytes)
return f"data:{mime_type};base64,{base64.b64encode(image_bytes).decode('utf-8')}"
def _first(value: object) -> object:
return value[0] if isinstance(value, list) and value else value
class FalAIImageEditConfig(BaseImageEditConfig):
"""
Image edits served through Fal AI's ``/edit`` endpoints, e.g. openai/gpt-image-2.5/flare/edit.
Fal expects a JSON body with ``image_urls`` (and an optional ``mask_url``) rather than multipart
uploads, so local files are sent inline as base64 data URLs.
"""
def get_supported_openai_params(self, model: str) -> list: # mutable-ok: base class contract returns a list
return list(SUPPORTED_OPENAI_PARAMS) # mutable-ok: base class contract returns a list
def map_openai_params( # mutable-ok: base class contract returns a dict
self,
image_edit_optional_params: ImageEditOptionalRequestParams,
model: str,
drop_params: bool,
) -> dict:
return { # mutable-ok: base class contract returns a dict
PARAM_TRANSLATION.get(key, key): self._translate_value(key, value, model)
for key, value in image_edit_optional_params.items()
if value is not None
}
def _translate_value(self, key: str, value: object, model: str) -> object:
if key == "size":
return map_gpt_image_size(value)
if key == "quality":
return map_gpt_image_quality(value, model)
return value
def validate_environment(
self,
headers: dict,
model: str,
api_key: str | None = None,
litellm_params: dict | None = None,
api_base: str | None = None,
) -> dict:
final_api_key: Final = api_key or get_secret_str("FAL_AI_API_KEY")
if not final_api_key:
raise ValueError("FAL_AI_API_KEY is not set")
return {**headers, "Authorization": f"Key {final_api_key}"} # mutable-ok: base class contract returns a dict
def use_multipart_form_data(self) -> bool:
return False
def get_complete_url(
self,
model: str,
api_base: str | None,
litellm_params: dict,
) -> str:
base_url: Final = (api_base or get_secret_str("FAL_AI_API_BASE") or DEFAULT_BASE_URL).rstrip("/")
endpoint: Final = model if model.endswith(EDIT_SUFFIX) else f"{model}{EDIT_SUFFIX}"
return f"{base_url}/{endpoint}"
def transform_image_edit_request(
self,
model: str,
prompt: str | None,
image: FileTypes | None,
image_edit_optional_request_params: dict,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> tuple[dict, RequestFiles]:
if image is None:
raise ValueError("Fal AI image edit requires at least one input image")
images: Final = tuple(image) if isinstance(image, list) else (image,)
mask: Final = _first(image_edit_optional_request_params.get("mask"))
mask_field: Final[Mapping[str, str]] = (
MappingProxyType({"mask_url": _to_data_url(mask)}) if mask is not None else MappingProxyType({})
)
provider_params: Final[Mapping[str, object]] = MappingProxyType(
{
key: value for key, value in image_edit_optional_request_params.items() if key != "mask"
} # mutable-ok: frozen by MappingProxyType
)
request_body: Final[dict[str, object]] = { # mutable-ok: base class contract returns a dict
"prompt": prompt,
"image_urls": tuple(_to_data_url(img) for img in images if img is not None),
**mask_field,
**provider_params,
}
return request_body, ()
def transform_image_edit_response(
self,
model: str,
raw_response: httpx.Response,
logging_obj: "LiteLLMLoggingObj",
) -> ImageResponse:
try:
response_json: Final = raw_response.json()
except Exception as e:
raise self.get_error_class(
error_message=f"Error parsing Fal AI image edit response: {e}",
status_code=raw_response.status_code,
headers=raw_response.headers,
)
model_response: Final = ImageResponse()
model_response.data = list( # mutable-ok: ImageResponse.data is typed as a list
fal_images_to_image_objects(response_json.get("images", ()))
)
return model_response

View file

@ -9,6 +9,7 @@ from .bytedance_transformation import (
FalAIBytedanceDreaminaV31Config,
FalAIBytedanceSeedreamV3Config,
)
from .flux_dev_transformation import FalAIFluxDevConfig
from .flux_pro_v11_transformation import FalAIFluxProV11Config
from .flux_pro_v11_ultra_transformation import FalAIFluxProV11UltraConfig
from .flux_schnell_transformation import FalAIFluxSchnellConfig
@ -25,6 +26,7 @@ __all__ = [
"FalAIBriaConfig",
"FalAIBytedanceDreaminaV31Config",
"FalAIBytedanceSeedreamV3Config",
"FalAIFluxDevConfig",
"FalAIFluxProV11Config",
"FalAIFluxProV11UltraConfig",
"FalAIFluxSchnellConfig",
@ -65,6 +67,8 @@ def get_fal_ai_image_generation_config(model: str) -> BaseImageGenerationConfig:
if "ultra" in model_lower:
return FalAIFluxProV11UltraConfig()
return FalAIFluxProV11Config()
elif "flux/dev" in model_lower or "flux-dev" in model_lower:
return FalAIFluxDevConfig()
elif "flux/schnell" in model_lower or "flux-schnell" in model_lower or "schnell" in model_lower:
return FalAIFluxSchnellConfig()
elif "bytedance/seedream" in model_lower:

View file

@ -0,0 +1,12 @@
from .flux_schnell_transformation import FalAIFluxSchnellConfig
class FalAIFluxDevConfig(FalAIFluxSchnellConfig):
"""
Configuration for Fal AI Flux Dev model.
Model endpoint: fal-ai/flux/dev
Documentation: https://fal.ai/models/fal-ai/flux/dev
"""
IMAGE_GENERATION_ENDPOINT: str = "fal-ai/flux/dev"

View file

@ -22,6 +22,32 @@ SUPPORTED_OPENAI_PARAMS: Final[tuple[OpenAIImageGenerationOptionalParams, ...]]
"response_format",
"size",
)
SUPPORTED_QUALITIES: Final[frozenset[str]] = frozenset({"auto", "low", "medium", "high"})
GPT_IMAGE_25_QUALITIES: Final[frozenset[str]] = SUPPORTED_QUALITIES | frozenset(("xhigh", "max"))
GPT_IMAGE_25_MARKER: Final[str] = "gpt-image-2.5"
OPENAI_QUALITY_ALIASES: Final[Mapping[str, str]] = MappingProxyType({"hd": "high", "standard": "medium"})
def map_gpt_image_size(size: object) -> object:
if not isinstance(size, str) or size == "auto":
return size
try:
width, height = (int(part) for part in size.lower().split("x"))
except ValueError:
return size
image_size: Final[FalAIImageSize] = {"width": width, "height": height}
return image_size
def supported_gpt_image_qualities(model: str) -> frozenset[str]:
return GPT_IMAGE_25_QUALITIES if GPT_IMAGE_25_MARKER in model.lower() else SUPPORTED_QUALITIES
def map_gpt_image_quality(quality: object, model: str) -> object:
if not isinstance(quality, str):
return quality
normalized: Final[str] = OPENAI_QUALITY_ALIASES.get(quality, quality)
return normalized if normalized in supported_gpt_image_qualities(model) else "auto"
class FalAIGPTImage2Config(FalAIBaseConfig):
@ -31,13 +57,12 @@ class FalAIGPTImage2Config(FalAIBaseConfig):
Model endpoints:
- openai/gpt-image-2 (text-to-image)
- openai/gpt-image-2/edit (editing, with optional mask)
- openai/gpt-image-2.5/flare/text-to-image, openai/gpt-image-2.5/sunburst/text-to-image
Documentation: https://fal.ai/models/openai/gpt-image-2/api
"""
MODEL_PREFIX: Final[str] = "openai/"
SUPPORTED_QUALITIES: Final[frozenset[str]] = frozenset({"auto", "low", "medium", "high"})
OPENAI_QUALITY_ALIASES: Final[Mapping[str, str]] = MappingProxyType({"hd": "high", "standard": "medium"})
PARAM_TRANSLATION: Final[Mapping[str, str]] = MappingProxyType(
{
"n": "num_images",
@ -83,36 +108,20 @@ class FalAIGPTImage2Config(FalAIBaseConfig):
)
translated_params: Final[Mapping[str, object]] = MappingProxyType(
{
self.PARAM_TRANSLATION[key]: self._translate_value(key, value)
self.PARAM_TRANSLATION[key]: self._translate_value(key, value, model)
for key, value in non_default_params.items()
if key in self.PARAM_TRANSLATION and self.PARAM_TRANSLATION[key] not in optional_params
}
)
return {**optional_params, **translated_params} # mutable-ok: base class contract returns a dict
def _translate_value(self, key: str, value: object) -> object:
def _translate_value(self, key: str, value: object, model: str) -> object:
if key == "size":
return self._map_image_size(value)
return map_gpt_image_size(value)
if key == "quality":
return self._map_quality(value)
return map_gpt_image_quality(value, model)
return value
def _map_image_size(self, size: object) -> object:
if not isinstance(size, str) or size == "auto":
return size
try:
width, height = (int(part) for part in size.lower().split("x"))
except ValueError:
return size
image_size: Final[FalAIImageSize] = {"width": width, "height": height}
return image_size
def _map_quality(self, quality: object) -> object:
if not isinstance(quality, str):
return quality
normalized: Final[str] = self.OPENAI_QUALITY_ALIASES.get(quality, quality)
return normalized if normalized in self.SUPPORTED_QUALITIES else "auto"
def transform_image_generation_request( # mutable-ok: base class contract returns a dict
self,
model: str,

View file

@ -22,6 +22,18 @@ else:
LiteLLMLoggingObj = Any
def fal_images_to_image_objects(images: object) -> tuple[ImageObject, ...]:
if not isinstance(images, list):
return ()
return tuple(
ImageObject(url=image_data.get("url", None), b64_json=image_data.get("b64_json", None))
if isinstance(image_data, dict)
else ImageObject(url=image_data, b64_json=None)
for image_data in images
if isinstance(image_data, (dict, str))
)
class FalAIBaseConfig(BaseImageGenerationConfig):
"""
Base configuration for Fal AI image generation models.
@ -96,26 +108,7 @@ class FalAIBaseConfig(BaseImageGenerationConfig):
if not model_response.data:
model_response.data = []
# Handle fal.ai response format
images: Final = response_data.get("images", [])
if isinstance(images, list):
for image_data in images:
if isinstance(image_data, dict):
model_response.data.append(
ImageObject(
url=image_data.get("url", None),
b64_json=image_data.get("b64_json", None),
)
)
elif isinstance(image_data, str):
# If images is just a list of URLs
model_response.data.append(
ImageObject(
url=image_data,
b64_json=None,
)
)
model_response.data.extend(fal_images_to_image_objects(response_data.get("images", ())))
return model_response

File diff suppressed because it is too large Load diff

View file

@ -9508,6 +9508,10 @@ class ProviderConfigManager:
)
return BlackForestLabsImageEditConfig()
elif LlmProviders.FAL_AI == provider:
from litellm.llms.fal_ai.image_edit import FalAIImageEditConfig
return FalAIImageEditConfig()
elif LlmProviders.AZURE_AI == provider:
from litellm.llms.azure_ai.image_edit import get_azure_ai_image_edit_config

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
import base64
import io
import json
import httpx
import pytest
from litellm.llms.fal_ai.image_edit import FalAIImageEditConfig
from litellm.types.images.main import ImageEditOptionalRequestParams
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import ImageResponse, LlmProviders
from litellm.utils import ProviderConfigManager
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
def test_fal_ai_resolves_to_image_edit_config():
config = ProviderConfigManager.get_provider_image_edit_config(
model="openai/gpt-image-2.5/flare/edit", provider=LlmProviders.FAL_AI
)
assert isinstance(config, FalAIImageEditConfig)
@pytest.mark.parametrize(
"model,expected",
[
("openai/gpt-image-2.5/flare", "https://fal.run/openai/gpt-image-2.5/flare/edit"),
("openai/gpt-image-2.5/sunburst/edit", "https://fal.run/openai/gpt-image-2.5/sunburst/edit"),
("openai/gpt-image-2", "https://fal.run/openai/gpt-image-2/edit"),
],
)
def test_get_complete_url_appends_edit_suffix_once(model, expected):
assert FalAIImageEditConfig().get_complete_url(model=model, api_base=None, litellm_params={}) == expected
def test_get_complete_url_respects_api_base():
url = FalAIImageEditConfig().get_complete_url(
model="openai/gpt-image-2.5/flare", api_base="https://proxy.internal/", litellm_params={}
)
assert url == "https://proxy.internal/openai/gpt-image-2.5/flare/edit"
def test_validate_environment_uses_fal_key_scheme():
headers = FalAIImageEditConfig().validate_environment(headers={}, model="m", api_key="secret")
assert headers["Authorization"] == "Key secret"
def test_validate_environment_requires_key(monkeypatch):
monkeypatch.delenv("FAL_AI_API_KEY", raising=False)
with pytest.raises(ValueError, match="FAL_AI_API_KEY"):
FalAIImageEditConfig().validate_environment(headers={}, model="m", api_key=None)
def test_map_openai_params_translates_to_fal_names():
mapped = FalAIImageEditConfig().map_openai_params(
image_edit_optional_params=ImageEditOptionalRequestParams(
n=2, size="1024x1536", quality="xhigh", background="transparent"
),
model="openai/gpt-image-2.5/flare/edit",
drop_params=False,
)
assert mapped == {
"num_images": 2,
"image_size": {"width": 1024, "height": 1536},
"quality": "xhigh",
"background": "transparent",
}
def test_transform_request_inlines_local_images_as_data_urls_and_keeps_remote_urls():
body, files = FalAIImageEditConfig().transform_image_edit_request(
model="openai/gpt-image-2.5/flare/edit",
prompt="make it blue",
image=[io.BytesIO(PNG_BYTES), "https://example.com/in.png"],
image_edit_optional_request_params={"num_images": 1, "mask": io.BytesIO(PNG_BYTES)},
litellm_params=GenericLiteLLMParams(),
headers={},
)
expected_data_url = "data:image/png;base64," + base64.b64encode(PNG_BYTES).decode()
assert files == ()
assert body["prompt"] == "make it blue"
assert json.loads(json.dumps(body))["image_urls"] == [expected_data_url, "https://example.com/in.png"]
assert body["mask_url"] == expected_data_url
assert body["num_images"] == 1
assert "mask" not in body
def test_transform_response_maps_fal_images():
raw = httpx.Response(200, json={"images": [{"url": "https://fal.media/out.png"}]})
response = FalAIImageEditConfig().transform_image_edit_response(
model="openai/gpt-image-2.5/flare/edit", raw_response=raw, logging_obj=None
)
assert isinstance(response, ImageResponse)
assert [image.url for image in response.data] == ["https://fal.media/out.png"]
def test_transform_request_requires_an_image():
with pytest.raises(ValueError, match="input image"):
FalAIImageEditConfig().transform_image_edit_request(
model="openai/gpt-image-2.5/flare/edit",
prompt="make it blue",
image=None,
image_edit_optional_request_params={},
litellm_params=GenericLiteLLMParams(),
headers={},
)

View file

@ -0,0 +1,61 @@
import httpx
import pytest
from litellm.llms.fal_ai.image_generation import (
FalAIFluxDevConfig,
FalAIFluxSchnellConfig,
FalAIImageGenerationConfig,
get_fal_ai_image_generation_config,
)
from litellm.types.utils import ImageResponse
@pytest.mark.parametrize("model", ["fal-ai/flux/dev", "flux/dev", "flux-dev"])
def test_flux_dev_config_selected(model):
config = get_fal_ai_image_generation_config(model)
assert isinstance(config, FalAIFluxDevConfig)
assert not isinstance(config, FalAIImageGenerationConfig)
def test_flux_schnell_still_routes_to_schnell():
config = get_fal_ai_image_generation_config("fal-ai/flux/schnell")
assert isinstance(config, FalAIFluxSchnellConfig)
assert not isinstance(config, FalAIFluxDevConfig)
def test_flux_dev_url_targets_dev_endpoint():
url = FalAIFluxDevConfig().get_complete_url(
api_base=None, api_key="k", model="fal-ai/flux/dev", optional_params={}, litellm_params={}
)
assert url == "https://fal.run/fal-ai/flux/dev"
def test_flux_dev_maps_openai_params_and_builds_request():
config = FalAIFluxDevConfig()
optional_params = config.map_openai_params(
non_default_params={"n": 2, "size": "1024x1024", "response_format": "b64_json"},
optional_params={},
model="fal-ai/flux/dev",
drop_params=False,
)
body = config.transform_image_generation_request(
model="fal-ai/flux/dev", prompt="a cat", optional_params=optional_params, litellm_params={}, headers={}
)
assert body["prompt"] == "a cat"
assert body["num_images"] == 2
assert body["image_size"] == "square_hd"
def test_flux_dev_response_yields_one_image_object_per_fal_image():
raw = httpx.Response(200, json={"images": [{"url": "https://fal.media/a.png"}, {"url": "https://fal.media/b.png"}]})
response = FalAIFluxDevConfig().transform_image_generation_response(
model="fal-ai/flux/dev",
raw_response=raw,
model_response=ImageResponse(),
logging_obj=None,
request_data={},
optional_params={},
litellm_params={},
encoding=None,
)
assert [image.url for image in response.data] == ["https://fal.media/a.png", "https://fal.media/b.png"]

View file

@ -127,3 +127,36 @@ def test_transform_image_generation_request():
) == {"prompt": "a red bicycle", "quality": "high", "num_images": 2}
@pytest.mark.parametrize(
"model",
[
"openai/gpt-image-2.5/flare/text-to-image",
"openai/gpt-image-2.5/sunburst/text-to-image",
],
)
def test_gpt_image_25_routes_to_its_own_fal_endpoint(model):
config = get_fal_ai_image_generation_config(model)
assert isinstance(config, FalAIGPTImage2Config)
assert (
config.get_complete_url(api_base=None, api_key="k", model=model, optional_params={}, litellm_params={})
== f"https://fal.run/{model}"
)
@pytest.mark.parametrize(
"model,quality,expected",
[
("openai/gpt-image-2.5/flare/text-to-image", "xhigh", "xhigh"),
("openai/gpt-image-2.5/sunburst/text-to-image", "max", "max"),
("openai/gpt-image-2.5/flare/text-to-image", "hd", "high"),
("openai/gpt-image-2", "xhigh", "auto"),
("openai/gpt-image-2", "max", "auto"),
],
)
def test_map_openai_params_quality_tiers_follow_model(model, quality, expected):
assert FalAIGPTImage2Config().map_openai_params(
non_default_params={"quality": quality},
optional_params={},
model=model,
drop_params=False,
) == {"quality": expected}

View file

@ -17,3 +17,74 @@ def _use_local_model_cost_map(monkeypatch):
def _image_response(num_images: int = 1) -> ImageResponse:
return ImageResponse(data=[ImageObject(url="https://example.com/img.png") for _ in range(num_images)])
GPT_IMAGE_25_MODELS = (
"openai/gpt-image-2.5/flare/text-to-image",
"openai/gpt-image-2.5/flare/edit",
"openai/gpt-image-2.5/sunburst/text-to-image",
"openai/gpt-image-2.5/sunburst/edit",
)
@pytest.mark.parametrize("model", GPT_IMAGE_25_MODELS)
def test_gpt_image_25_default_request_matches_high_1024x768_keyed_row(model):
default_cost = cost_calculator(model=f"fal_ai/{model}", image_response=_image_response(), optional_params={})
keyed_cost = litellm.model_cost[f"fal_ai/high/1024-x-768/{model}"]["output_cost_per_image"]
assert default_cost == keyed_cost > 0
@pytest.mark.parametrize("model", GPT_IMAGE_25_MODELS)
def test_gpt_image_25_quality_and_size_pick_keyed_row(model):
cost = cost_calculator(
model=f"fal_ai/{model}",
image_response=_image_response(num_images=2),
optional_params={"quality": "max", "image_size": {"width": 3840, "height": 2160}},
)
assert cost == 2 * litellm.model_cost[f"fal_ai/max/3840-x-2160/{model}"]["output_cost_per_image"] > 0
def test_gpt_image_25_edit_auto_size_still_honors_quality():
model = "fal_ai/openai/gpt-image-2.5/flare/edit"
low = cost_calculator(
model=model, image_response=_image_response(), optional_params={"quality": "low", "image_size": "auto"}
)
high = cost_calculator(
model=model, image_response=_image_response(), optional_params={"quality": "high", "image_size": "auto"}
)
assert 0 < low < high
def test_gpt_image_25_quality_tiers_are_monotonic():
costs = tuple(
cost_calculator(
model="fal_ai/openai/gpt-image-2.5/sunburst/text-to-image",
image_response=_image_response(),
optional_params={"quality": quality, "image_size": "square_hd"},
)
for quality in ("low", "medium", "high", "xhigh", "max")
)
assert costs == tuple(sorted(costs)) and len(set(costs)) == len(costs)
def test_flux_dev_cost_is_nonzero_and_distinct_from_schnell():
dev = cost_calculator(
model="fal_ai/fal-ai/flux/dev", image_response=_image_response(num_images=3), optional_params={}
)
schnell = cost_calculator(
model="fal_ai/fal-ai/flux/schnell", image_response=_image_response(num_images=3), optional_params={}
)
assert dev > schnell > 0
assert dev == 3 * litellm.model_cost["fal_ai/fal-ai/flux/dev"]["output_cost_per_image"]
def test_image_edit_call_type_routes_to_fal_keyed_pricing():
model = "openai/gpt-image-2.5/flare/edit"
cost = CostCalculatorUtils.route_image_generation_cost_calculator(
model=model,
completion_response=_image_response(),
custom_llm_provider="fal_ai",
optional_params={"quality": "medium", "image_size": {"width": 1024, "height": 1024}},
call_type="aimage_edit",
)
assert cost == litellm.model_cost[f"fal_ai/medium/1024-x-1024/{model}"]["output_cost_per_image"] > 0