-
-
Notifications
You must be signed in to change notification settings - Fork 11.8k
gemini imageConfig forwarding in /images/generations and /images/edits #28192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||
| import base64 | ||||||||||||||
| import datetime | ||||||||||||||
| import math | ||||||||||||||
| from typing import Any, Dict, List, Optional, Union | ||||||||||||||
|
|
||||||||||||||
| import httpx | ||||||||||||||
|
|
@@ -13,6 +14,151 @@ | |||||||||||||
| from litellm.types.utils import TokenCountResponse | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| GEMINI_IMAGE_ASPECT_RATIOS: Dict[str, float] = { | ||||||||||||||
| "1:1": 1 / 1, | ||||||||||||||
| "1:4": 1 / 4, | ||||||||||||||
| "1:8": 1 / 8, | ||||||||||||||
| "2:3": 2 / 3, | ||||||||||||||
| "3:2": 3 / 2, | ||||||||||||||
| "3:4": 3 / 4, | ||||||||||||||
| "4:1": 4 / 1, | ||||||||||||||
| "4:3": 4 / 3, | ||||||||||||||
| "4:5": 4 / 5, | ||||||||||||||
| "5:4": 5 / 4, | ||||||||||||||
| "8:1": 8 / 1, | ||||||||||||||
| "9:16": 9 / 16, | ||||||||||||||
| "16:9": 16 / 9, | ||||||||||||||
| "21:9": 21 / 9, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| GEMINI_IMAGE_SIZE_TO_ASPECT_RATIO: Dict[tuple[int, int], str] = { | ||||||||||||||
| (512, 512): "1:1", | ||||||||||||||
| (1024, 1024): "1:1", | ||||||||||||||
| (2048, 2048): "1:1", | ||||||||||||||
| (4096, 4096): "1:1", | ||||||||||||||
| (256, 1024): "1:4", | ||||||||||||||
| (512, 2048): "1:4", | ||||||||||||||
| (1024, 4096): "1:4", | ||||||||||||||
| (2048, 8192): "1:4", | ||||||||||||||
| (192, 1536): "1:8", | ||||||||||||||
| (384, 3072): "1:8", | ||||||||||||||
| (768, 6144): "1:8", | ||||||||||||||
| (1536, 12288): "1:8", | ||||||||||||||
| (424, 632): "2:3", | ||||||||||||||
| (848, 1264): "2:3", | ||||||||||||||
| (1696, 2528): "2:3", | ||||||||||||||
| (3392, 5056): "2:3", | ||||||||||||||
| (632, 424): "3:2", | ||||||||||||||
| (1264, 848): "3:2", | ||||||||||||||
| (2528, 1696): "3:2", | ||||||||||||||
| (5056, 3392): "3:2", | ||||||||||||||
| (448, 600): "3:4", | ||||||||||||||
| (896, 1200): "3:4", | ||||||||||||||
| (1792, 2400): "3:4", | ||||||||||||||
| (3584, 4800): "3:4", | ||||||||||||||
| (1024, 256): "4:1", | ||||||||||||||
| (2048, 512): "4:1", | ||||||||||||||
| (4096, 1024): "4:1", | ||||||||||||||
| (8192, 2048): "4:1", | ||||||||||||||
| (600, 448): "4:3", | ||||||||||||||
| (1200, 896): "4:3", | ||||||||||||||
| (2400, 1792): "4:3", | ||||||||||||||
| (4800, 3584): "4:3", | ||||||||||||||
| (464, 576): "4:5", | ||||||||||||||
| (928, 1152): "4:5", | ||||||||||||||
| (1856, 2304): "4:5", | ||||||||||||||
| (3712, 4608): "4:5", | ||||||||||||||
| (576, 464): "5:4", | ||||||||||||||
| (1152, 928): "5:4", | ||||||||||||||
| (2304, 1856): "5:4", | ||||||||||||||
| (4608, 3712): "5:4", | ||||||||||||||
| (1536, 192): "8:1", | ||||||||||||||
| (3072, 384): "8:1", | ||||||||||||||
| (6144, 768): "8:1", | ||||||||||||||
| (12288, 1536): "8:1", | ||||||||||||||
| (384, 688): "9:16", | ||||||||||||||
| (768, 1376): "9:16", | ||||||||||||||
| (1536, 2752): "9:16", | ||||||||||||||
| (3072, 5504): "9:16", | ||||||||||||||
| (688, 384): "16:9", | ||||||||||||||
| (1376, 768): "16:9", | ||||||||||||||
| (2752, 1536): "16:9", | ||||||||||||||
| (5504, 3072): "16:9", | ||||||||||||||
| (792, 336): "21:9", | ||||||||||||||
| (1584, 672): "21:9", | ||||||||||||||
| (3168, 1344): "21:9", | ||||||||||||||
| (6336, 2688): "21:9", | ||||||||||||||
| (1280, 896): "4:3", | ||||||||||||||
| (896, 1280): "3:4", | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def map_openai_size_to_gemini_image_config( | ||||||||||||||
| size: str, model: str | ||||||||||||||
| ) -> Optional[Dict[str, str]]: | ||||||||||||||
| dimensions = _parse_openai_image_size(size) | ||||||||||||||
| if dimensions is None: | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
| width, height = dimensions | ||||||||||||||
| image_config = { | ||||||||||||||
| "aspectRatio": _map_dimensions_to_gemini_aspect_ratio(width, height) | ||||||||||||||
| } | ||||||||||||||
| if supports_gemini_image_size(model): | ||||||||||||||
| image_config["imageSize"] = _map_dimensions_to_gemini_image_size(width, height) | ||||||||||||||
| return image_config | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def supports_gemini_image_size(model: str) -> bool: | ||||||||||||||
| # Gemini 2.5 Flash image supports aspectRatio but rejects imageSize; newer | ||||||||||||||
| # Gemini image models are expected to support both fields. | ||||||||||||||
| return "2.5-flash" not in model | ||||||||||||||
|
Comment on lines
+112
to
+115
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Rule Used: What: Do not hardcode model-specific flags in the ... (source)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m going to keep the For image generation, this is also an older-model exception: Gemini 2.5 Flash image supports |
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _parse_openai_image_size(size: str) -> Optional[tuple[int, int]]: | ||||||||||||||
| if size == "auto": | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
| width_str, separator, height_str = size.lower().partition("x") | ||||||||||||||
| if not separator: | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
| try: | ||||||||||||||
| width = int(width_str) | ||||||||||||||
| height = int(height_str) | ||||||||||||||
| except ValueError: | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
| if width <= 0 or height <= 0: | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
| return width, height | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _map_dimensions_to_gemini_aspect_ratio(width: int, height: int) -> str: | ||||||||||||||
| if (width, height) in GEMINI_IMAGE_SIZE_TO_ASPECT_RATIO: | ||||||||||||||
| return GEMINI_IMAGE_SIZE_TO_ASPECT_RATIO[(width, height)] | ||||||||||||||
|
|
||||||||||||||
| requested_ratio = width / height | ||||||||||||||
| return min( | ||||||||||||||
| GEMINI_IMAGE_ASPECT_RATIOS, | ||||||||||||||
| key=lambda aspect_ratio: abs( | ||||||||||||||
| math.log(GEMINI_IMAGE_ASPECT_RATIOS[aspect_ratio] / requested_ratio) | ||||||||||||||
| ), | ||||||||||||||
| ) | ||||||||||||||
|
Comment on lines
+138
to
+148
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The old Rule Used: What: avoid backwards-incompatible changes without... (source)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in force-pushed commit |
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _map_dimensions_to_gemini_image_size(width: int, height: int) -> str: | ||||||||||||||
| effective_square_side = math.sqrt(width * height) | ||||||||||||||
| if effective_square_side < 768: | ||||||||||||||
| return "512" | ||||||||||||||
| if effective_square_side < 1536: | ||||||||||||||
| return "1K" | ||||||||||||||
| if effective_square_side < 3072: | ||||||||||||||
| return "2K" | ||||||||||||||
| return "4K" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class GeminiError(BaseLLMException): | ||||||||||||||
| pass | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,18 @@ | ||||||||||||||||||||||||||||
| import base64 | ||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||
| from io import BufferedReader, BytesIO | ||||||||||||||||||||||||||||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import httpx | ||||||||||||||||||||||||||||
| from httpx._types import RequestFiles | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import litellm | ||||||||||||||||||||||||||||
| from litellm.images.utils import ImageEditRequestUtils | ||||||||||||||||||||||||||||
| from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig | ||||||||||||||||||||||||||||
| from litellm.llms.gemini.common_utils import ( | ||||||||||||||||||||||||||||
| map_openai_size_to_gemini_image_config, | ||||||||||||||||||||||||||||
| supports_gemini_image_size, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from litellm.secret_managers.main import get_secret_str | ||||||||||||||||||||||||||||
| from litellm.types.images.main import ImageEditOptionalRequestParams | ||||||||||||||||||||||||||||
| from litellm.types.router import GenericLiteLLMParams | ||||||||||||||||||||||||||||
|
|
@@ -22,7 +28,7 @@ | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| class GeminiImageEditConfig(BaseImageEditConfig): | ||||||||||||||||||||||||||||
| DEFAULT_BASE_URL: str = "https://generativelanguage.googleapis.com/v1beta" | ||||||||||||||||||||||||||||
| SUPPORTED_PARAMS: List[str] = ["size"] | ||||||||||||||||||||||||||||
| SUPPORTED_PARAMS: List[str] = ["size", "imageConfig"] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def get_supported_openai_params(self, model: str) -> List[str]: | ||||||||||||||||||||||||||||
| return list(self.SUPPORTED_PARAMS) | ||||||||||||||||||||||||||||
|
|
@@ -43,9 +49,24 @@ def map_openai_params( | |||||||||||||||||||||||||||
| mapped_params: Dict[str, Any] = {} | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if "size" in filtered_params: | ||||||||||||||||||||||||||||
| mapped_params["aspectRatio"] = self._map_size_to_aspect_ratio( | ||||||||||||||||||||||||||||
| filtered_params["size"] # type: ignore[arg-type] | ||||||||||||||||||||||||||||
| image_config = map_openai_size_to_gemini_image_config( | ||||||||||||||||||||||||||||
| filtered_params["size"], # type: ignore[arg-type] | ||||||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| if image_config is not None: | ||||||||||||||||||||||||||||
| mapped_params["imageConfig"] = image_config | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| image_config_param = filtered_params.get("imageConfig") | ||||||||||||||||||||||||||||
| if isinstance(image_config_param, str): | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| image_config_param = json.loads(image_config_param) | ||||||||||||||||||||||||||||
| except json.JSONDecodeError as exc: | ||||||||||||||||||||||||||||
| raise litellm.UnsupportedParamsError( | ||||||||||||||||||||||||||||
| model=model, | ||||||||||||||||||||||||||||
| message="`imageConfig` must be valid JSON when provided as a string.", | ||||||||||||||||||||||||||||
| ) from exc | ||||||||||||||||||||||||||||
| if isinstance(image_config_param, dict): | ||||||||||||||||||||||||||||
| mapped_params["imageConfig"] = image_config_param | ||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return mapped_params | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -109,13 +130,12 @@ def transform_image_edit_request( # type: ignore[override] | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| generation_config: Dict[str, Any] = {} | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if "aspectRatio" in image_edit_optional_request_params: | ||||||||||||||||||||||||||||
| # Move aspectRatio into imageConfig inside generationConfig | ||||||||||||||||||||||||||||
| if "imageConfig" not in generation_config: | ||||||||||||||||||||||||||||
| generation_config["imageConfig"] = {} | ||||||||||||||||||||||||||||
| generation_config["imageConfig"]["aspectRatio"] = ( | ||||||||||||||||||||||||||||
| image_edit_optional_request_params["aspectRatio"] | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| if isinstance(image_edit_optional_request_params.get("imageConfig"), dict): | ||||||||||||||||||||||||||||
| image_config = dict(image_edit_optional_request_params["imageConfig"]) | ||||||||||||||||||||||||||||
| if not supports_gemini_image_size(model): | ||||||||||||||||||||||||||||
| image_config.pop("imageSize", None) | ||||||||||||||||||||||||||||
| if image_config: | ||||||||||||||||||||||||||||
| generation_config["imageConfig"] = image_config | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if generation_config: | ||||||||||||||||||||||||||||
| request_body["generationConfig"] = generation_config | ||||||||||||||||||||||||||||
|
|
@@ -158,16 +178,6 @@ def transform_image_edit_response( | |||||||||||||||||||||||||||
| model_response.data = cast(List[OpenAIImage], data_list) | ||||||||||||||||||||||||||||
| return model_response | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def _map_size_to_aspect_ratio(self, size: str) -> str: | ||||||||||||||||||||||||||||
| aspect_ratio_map = { | ||||||||||||||||||||||||||||
| "1024x1024": "1:1", | ||||||||||||||||||||||||||||
| "1792x1024": "16:9", | ||||||||||||||||||||||||||||
| "1024x1792": "9:16", | ||||||||||||||||||||||||||||
| "1280x896": "4:3", | ||||||||||||||||||||||||||||
| "896x1280": "3:4", | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return aspect_ratio_map.get(size, "1:1") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def _prepare_inline_image_parts( | ||||||||||||||||||||||||||||
| self, image: Union[FileTypes, List[FileTypes]] | ||||||||||||||||||||||||||||
| ) -> List[Dict[str, Any]]: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
supports_gemini_image_sizehard-codes"2.5-flash"as a string guard. Per the project's custom rule, model-specific capability flags must live inmodel_prices_and_context_window.jsonand be queried throughget_model_info, so that support for future models can be enabled without a code change. As written, any new model that also lacksimageSizesupport (or any future Gemini 2.5-flash variant with a slightly different name) will silently receiveimageConfigobjects that includeimageSize, potentially causing API errors.Rule Used: What: Do not hardcode model-specific flags in the ... (source)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m going to keep the
2.5-flashguard local here rather than adding a newmodel_prices_and_context_window.jsoncapability flag for this PR. This matches existing Gemini transformation patterns in this area:VertexGeminiConfig._is_gemini_3_or_newer()uses model-name detection for Gemini 3 behavior,_supports_penalty_parameters()has a model-name exception list, and the Gemini thinking mapping branches ongemini-2.5-flash-lite/gemini-2.5-pro/gemini-2.5-flashsubstrings.For image generation, this is also an older-model exception: Gemini 2.5 Flash image supports
aspectRatiobut notimageSize, while newer Gemini image models are expected to supportimageSize. Adding a new global model metadata flag for that narrow exception would add more surface area than this PR needs. I’ll add an inline code comment to make this intentional.