diff --git a/k8s/dev-config.yaml b/k8s/dev-config.yaml new file mode 100644 index 00000000000..2b10b4932e7 --- /dev/null +++ b/k8s/dev-config.yaml @@ -0,0 +1,112 @@ +# LiteLLM Proxy ConfigMap for dev namespace +# Apply: kubectl apply -f k8s/dev-config.yaml -n dev +# Restart: kubectl rollout restart deployment litellm -n dev +apiVersion: v1 +kind: ConfigMap +metadata: + name: litellm-config + namespace: dev +data: + config.yaml: | + model_list: + # --- FAL AI: Image Generation --- + - model_name: fal_ai/flux-2/klein/9b + litellm_params: + model: fal_ai/flux-2/klein/9b + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/flux-2/klein/9b/base/edit/lora + litellm_params: + model: fal_ai/flux-2/klein/9b/base/edit/lora + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/nano-banana-2 + litellm_params: + model: fal_ai/nano-banana-2 + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/nano-banana-2/edit + litellm_params: + model: fal_ai/nano-banana-2/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/nano-banana-pro + litellm_params: + model: fal_ai/nano-banana-pro + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/nano-banana-pro/edit + litellm_params: + model: fal_ai/nano-banana-pro/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/gemini-3-pro-image-preview + litellm_params: + model: fal_ai/gemini-3-pro-image-preview + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/gemini-3-pro-image-preview/edit + litellm_params: + model: fal_ai/gemini-3-pro-image-preview/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/gemini-3.1-flash-image-preview + litellm_params: + model: fal_ai/gemini-3.1-flash-image-preview + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/gemini-3.1-flash-image-preview/edit + litellm_params: + model: fal_ai/gemini-3.1-flash-image-preview/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/gemini-25-flash-image + litellm_params: + model: fal_ai/gemini-25-flash-image + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/gemini-25-flash-image/edit + litellm_params: + model: fal_ai/gemini-25-flash-image/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/bytedance/seedream/v5/lite/text-to-image + litellm_params: + model: fal_ai/bytedance/seedream/v5/lite/text-to-image + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/bytedance/seedream/v5/lite/edit + litellm_params: + model: fal_ai/bytedance/seedream/v5/lite/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/bytedance/seedream/v4.5/text-to-image + litellm_params: + model: fal_ai/bytedance/seedream/v4.5/text-to-image + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/bytedance/seedream/v4.5/edit + litellm_params: + model: fal_ai/bytedance/seedream/v4.5/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/bytedance/seedream/v4/text-to-image + litellm_params: + model: fal_ai/bytedance/seedream/v4/text-to-image + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/bytedance/seedream/v4/edit + litellm_params: + model: fal_ai/bytedance/seedream/v4/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/flux-pro/kontext + litellm_params: + model: fal_ai/flux-pro/kontext + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/flux-pro + litellm_params: + model: fal_ai/flux-pro + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/flux-2/klein/9b/edit + litellm_params: + model: fal_ai/flux-2/klein/9b/edit + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/kling-image/omni + litellm_params: + model: fal_ai/kling-image/omni + api_key: os.environ/FAL_AI_API_KEY + - model_name: fal_ai/iclight-v2 + litellm_params: + model: fal_ai/iclight-v2 + api_key: os.environ/FAL_AI_API_KEY + + general_settings: + master_key: os.environ/LITELLM_MASTER_KEY + database_url: os.environ/DATABASE_URL + + litellm_settings: + drop_params: true + telemetry: false diff --git a/litellm/llms/fal_ai/image_edit/__init__.py b/litellm/llms/fal_ai/image_edit/__init__.py new file mode 100644 index 00000000000..34ee7893ffd --- /dev/null +++ b/litellm/llms/fal_ai/image_edit/__init__.py @@ -0,0 +1,19 @@ +from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig + +from .transformation import FalAIImageEditConfig + +__all__ = [ + "FalAIImageEditConfig", + "get_fal_ai_image_edit_config", +] + + +def get_fal_ai_image_edit_config(model: str) -> BaseImageEditConfig: + """ + Get the appropriate Fal AI image edit configuration based on the model. + + Currently all Fal AI edit models use the same generic config since they + share the same JSON request/response format. Model-specific configs can + be added here in the future if needed. + """ + return FalAIImageEditConfig() diff --git a/litellm/llms/fal_ai/image_edit/transformation.py b/litellm/llms/fal_ai/image_edit/transformation.py new file mode 100644 index 00000000000..c8fc75211cb --- /dev/null +++ b/litellm/llms/fal_ai/image_edit/transformation.py @@ -0,0 +1,251 @@ +""" +Fal AI Image Edit Configuration + +Handles transformation between OpenAI-compatible format and Fal AI API format +for image editing endpoints (nano-banana-2/edit, gemini-3-pro-image-preview/edit, etc.). + +Fal AI edit endpoints accept JSON with prompt + image_url fields and return +{"images": [{"url": "..."}]} synchronously via fal.run — no polling required. +""" + +import base64 +import time +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +import httpx +from httpx._types import RequestFiles + +from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH +from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig +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, ImageObject, ImageResponse + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + + LiteLLMLoggingObj = _LiteLLMLoggingObj +else: + LiteLLMLoggingObj = Any + + +DEFAULT_BASE_URL = "https://fal.run" + + +class FalAIImageEditConfig(BaseImageEditConfig): + """ + Configuration for Fal AI image editing. + + Supports any Fal AI model that accepts edit-style requests + (prompt + image_url in JSON body). URL is constructed dynamically + from the model name: fal_ai/{path} -> https://fal.run/fal-ai/{path} + + HTTP requests are handled by the generic llm_http_handler (no polling needed). + This class only handles data transformation. + """ + + def get_supported_openai_params(self, model: str) -> List[str]: + return [ + "n", + "size", + "response_format", + ] + + def map_openai_params( + self, + image_edit_optional_params: ImageEditOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + optional_params: Dict[str, Any] = {} + params_dict = dict(image_edit_optional_params) + + if params_dict.get("n") is not None: + optional_params["num_images"] = params_dict["n"] + + if params_dict.get("size") is not None: + optional_params["image_size"] = self._map_image_size(params_dict["size"]) + + return optional_params + + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + final_api_key: Optional[str] = api_key or get_secret_str("FAL_AI_API_KEY") + if not final_api_key: + raise ValueError("FAL_AI_API_KEY is not set") + + headers["Authorization"] = f"Key {final_api_key}" + headers["Content-Type"] = "application/json" + headers["Accept"] = "application/json" + return headers + + def use_multipart_form_data(self) -> bool: + return False + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + base_url: str = ( + api_base or get_secret_str("FAL_AI_API_BASE") or DEFAULT_BASE_URL + ) + base_url = base_url.rstrip("/") + # model arrives without provider prefix (e.g. "nano-banana-2/edit") + # FAL API expects fal-ai/ prefix in the URL path + return f"{base_url}/fal-ai/{model}" + + def _read_image_bytes( + self, + image: Any, + depth: int = 0, + max_depth: int = DEFAULT_MAX_RECURSE_DEPTH, + ) -> bytes: + """Read image bytes from various input types.""" + if depth > max_depth: + raise ValueError( + f"Max recursion depth {max_depth} reached while reading image bytes." + ) + if isinstance(image, bytes): + return image + elif isinstance(image, list): + return self._read_image_bytes( + image[0], depth=depth + 1, max_depth=max_depth + ) + elif isinstance(image, str): + if image.startswith(("http://", "https://")): + response = httpx.get(image, timeout=60.0) + response.raise_for_status() + return response.content + else: + with open(image, "rb") as f: + return f.read() + elif hasattr(image, "read"): + pos = getattr(image, "tell", lambda: 0)() + if hasattr(image, "seek"): + image.seek(0) + data = image.read() + if hasattr(image, "seek"): + image.seek(pos) + return data + else: + raise ValueError( + f"Unsupported image type: {type(image)}. " + "Expected bytes, str (URL or file path), or file-like object." + ) + + def transform_image_edit_request( + self, + model: str, + prompt: Optional[str], + image: Optional[FileTypes], + image_edit_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[Dict, RequestFiles]: + """ + Transform OpenAI-style request to Fal AI request format. + + Fal AI edit endpoints accept JSON with image_url (data URI) + prompt. + """ + request_body: Dict[str, Any] = {} + + if prompt is not None: + request_body["prompt"] = prompt + + # Encode the input image as a data URI for Fal AI + if image is not None: + image_bytes = self._read_image_bytes(image) + b64_image = base64.b64encode(image_bytes).decode("utf-8") + data_uri = f"data:image/png;base64,{b64_image}" + request_body["image_url"] = data_uri + + # Pass through mapped optional params + for key, value in image_edit_optional_request_params.items(): + if key not in ("extra_headers",) and value is not None: + request_body[key] = value + + # Fal AI uses JSON, not multipart + return request_body, [] + + def transform_image_edit_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> ImageResponse: + """ + Transform Fal AI response to OpenAI-compatible ImageResponse. + + Fal AI returns: {"images": [{"url": "..."}]} + """ + try: + response_data = raw_response.json() + except Exception as e: + raise ValueError( + f"Error parsing Fal AI response (status={raw_response.status_code}): {e}" + ) + + image_objects: List[ImageObject] = [] + images = response_data.get("images", []) + if isinstance(images, list): + for img in images: + if isinstance(img, dict): + image_objects.append( + ImageObject( + url=img.get("url"), + b64_json=img.get("b64_json"), + ) + ) + elif isinstance(img, str): + image_objects.append(ImageObject(url=img)) + + # Some Fal models return a single "image" instead of "images" + if not image_objects: + single_image = response_data.get("image") + if isinstance(single_image, dict): + image_objects.append( + ImageObject( + url=single_image.get("url"), + b64_json=single_image.get("b64_json"), + ) + ) + elif isinstance(single_image, str): + image_objects.append(ImageObject(url=single_image)) + + if not image_objects: + raise ValueError( + f"No images in Fal AI response: {response_data}" + ) + + return ImageResponse( + created=int(time.time()), + data=image_objects, + ) + + @staticmethod + def _map_image_size(size: Any) -> Any: + """Map OpenAI size format (e.g. '1024x1024') to Fal AI image_size.""" + size_map = { + "1024x1024": "square_hd", + "512x512": "square", + "1792x1024": "landscape_16_9", + "1024x1792": "portrait_16_9", + "1024x768": "landscape_4_3", + "768x1024": "portrait_4_3", + } + if isinstance(size, str) and size in size_map: + return size_map[size] + if isinstance(size, str) and "x" in size: + try: + w, h = size.split("x") + return {"width": int(w), "height": int(h)} + except (ValueError, AttributeError): + pass + return size diff --git a/litellm/llms/fal_ai/image_generation/transformation.py b/litellm/llms/fal_ai/image_generation/transformation.py index 4a0dea48a10..815e5814a8b 100644 --- a/litellm/llms/fal_ai/image_generation/transformation.py +++ b/litellm/llms/fal_ai/image_generation/transformation.py @@ -50,6 +50,10 @@ class FalAIBaseConfig(BaseImageGenerationConfig): complete_url = complete_url.rstrip("/") if self.IMAGE_GENERATION_ENDPOINT: complete_url = f"{complete_url}/{self.IMAGE_GENERATION_ENDPOINT}" + else: + # Generic models need the model name in the URL path + # model arrives without provider prefix (e.g. "nano-banana-2/edit") + complete_url = f"{complete_url}/fal-ai/{model}" return complete_url def validate_environment( @@ -128,12 +132,31 @@ class FalAIImageGenerationConfig(FalAIBaseConfig): self, model: str ) -> List[OpenAIImageGenerationOptionalParams]: """ - Get supported OpenAI parameters for fal.ai image generation + Get supported OpenAI parameters for fal.ai image generation. + + FAL models accept many model-specific params (loras, guidance_scale, etc.) + that vary by model. We list known params explicitly and pass through all + others in map_openai_params to avoid silently dropping them. """ return [ "n", "response_format", "size", + # FAL-specific params used across models + "image_url", + "image_urls", + "loras", + "num_inference_steps", + "guidance_scale", + "output_format", + "image_size", + "aspect_ratio", + "enable_safety_checker", + "seed", + "strength", + "num_images", + "expand_prompt", + "safety_tolerance", ] def map_openai_params( @@ -143,18 +166,11 @@ class FalAIImageGenerationConfig(FalAIBaseConfig): model: str, drop_params: bool, ) -> dict: - supported_params = self.get_supported_openai_params(model) - for k in non_default_params.keys(): - if k not in optional_params.keys(): - if k in supported_params: - optional_params[k] = non_default_params[k] - elif drop_params: - pass - else: - raise ValueError( - f"Parameter {k} is not supported for model {model}. Supported parameters are {supported_params}. Set drop_params=True to drop unsupported parameters." - ) - + """Pass through all params for FAL. FAL models accept diverse model-specific + params that cannot be enumerated ahead of time.""" + for k, v in non_default_params.items(): + if k not in optional_params: + optional_params[k] = v return optional_params def transform_image_generation_request( diff --git a/litellm/utils.py b/litellm/utils.py index 8d55783bf9c..6169e7bc733 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -9030,6 +9030,10 @@ class ProviderConfigManager: ) return BlackForestLabsImageEditConfig() + elif LlmProviders.FAL_AI == provider: + from litellm.llms.fal_ai.image_edit import get_fal_ai_image_edit_config + + return get_fal_ai_image_edit_config(model) elif LlmProviders.AZURE_AI == provider: from litellm.llms.azure_ai.image_edit import get_azure_ai_image_edit_config