feat(vertex_ai): support image_size (2K/4K) for Gemini image generation

Fixes #24621

The Gemini API supports `imageConfig.imageSize` to control output
resolution (e.g., "2K", "4K"), but LiteLLM had no way to pass this
through. The `extra_body` approach doesn't work because
`generationConfig` is rebuilt from scratch in the transformation layer.

Changes:
- Vertex AI Gemini image edit: add `imageSize` to `SUPPORTED_PARAMS`
  and include it in `generationConfig.image_config.image_size`
- Google AI Studio image gen: same support for Gemini models
- Both: accept `size` param as either OpenAI format ("1024x1024" ->
  aspect_ratio) or Gemini format ("2K" -> image_size)
- Both: map OpenAI `quality="hd"` to `imageSize="2K"` as a convenient
  alternative
- Add tests for imageSize, combined aspect_ratio+imageSize, size="2K",
  and quality="hd" mappings
This commit is contained in:
Albert Sebastian 2026-04-08 15:27:34 +05:30
parent 5e80e075c7
commit b60e30a58b
3 changed files with 112 additions and 8 deletions

View file

@ -36,7 +36,7 @@ class GoogleImageGenConfig(BaseImageGenerationConfig):
Google AI Imagen API supported parameters
https://ai.google.dev/gemini-api/docs/imagen
"""
return ["n", "size"]
return ["n", "size", "quality"]
def map_openai_params(
self,
@ -55,8 +55,17 @@ class GoogleImageGenConfig(BaseImageGenerationConfig):
if k == "n":
mapped_params["sampleCount"] = v
elif k == "size":
# Map OpenAI size format to Google aspectRatio
mapped_params["aspectRatio"] = self._map_size_to_aspect_ratio(v)
if isinstance(v, str) and "x" in v:
# OpenAI format like "1024x1024" -> map to aspect ratio
mapped_params["aspectRatio"] = self._map_size_to_aspect_ratio(v)
else:
# Gemini image_size format like "1K", "2K", "4K"
mapped_params["imageSize"] = v
elif k == "quality":
# Map OpenAI quality to Gemini imageSize
# "hd" -> "2K", "standard" -> "1K"
if v == "hd" and "imageSize" not in mapped_params:
mapped_params["imageSize"] = "2K"
else:
mapped_params[k] = v
return mapped_params
@ -180,9 +189,20 @@ class GoogleImageGenConfig(BaseImageGenerationConfig):
"""
# For Gemini Flash Image Preview models, use standard Gemini format
if "gemini" in model:
generation_config: dict = {"response_modalities": ["IMAGE", "TEXT"]}
# Build image_config from mapped params (aspectRatio, imageSize)
image_config: dict = {}
if "aspectRatio" in optional_params:
image_config["aspect_ratio"] = optional_params["aspectRatio"]
if "imageSize" in optional_params:
image_config["image_size"] = optional_params["imageSize"]
if image_config:
generation_config["image_config"] = image_config
request_body: dict = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"response_modalities": ["IMAGE", "TEXT"]},
"generationConfig": generation_config,
}
return request_body
else:

View file

@ -32,7 +32,7 @@ class VertexAIGeminiImageEditConfig(BaseImageEditConfig, VertexLLM):
Uses generateContent API for Gemini models on Vertex AI
"""
SUPPORTED_PARAMS: List[str] = ["size"]
SUPPORTED_PARAMS: List[str] = ["size", "quality"]
def __init__(self) -> None:
BaseImageEditConfig.__init__(self)
@ -57,9 +57,22 @@ class VertexAIGeminiImageEditConfig(BaseImageEditConfig, VertexLLM):
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]
)
size_value = filtered_params["size"]
if isinstance(size_value, str) and "x" in size_value:
# OpenAI format like "1024x1024" -> map to aspect ratio
mapped_params["aspectRatio"] = self._map_size_to_aspect_ratio(
size_value
)
else:
# Gemini image_size format like "1K", "2K", "4K"
mapped_params["imageSize"] = size_value
if "quality" in filtered_params:
# Map OpenAI quality to Gemini imageSize
# "hd" -> "2K", "standard" -> "1K"
quality = filtered_params["quality"]
if quality == "hd" and "imageSize" not in mapped_params:
mapped_params["imageSize"] = "2K"
return mapped_params
@ -195,6 +208,10 @@ class VertexAIGeminiImageEditConfig(BaseImageEditConfig, VertexLLM):
image_config["aspect_ratio"] = image_edit_optional_request_params[
"aspectRatio"
]
if "imageSize" in image_edit_optional_request_params:
image_config["image_size"] = image_edit_optional_request_params[
"imageSize"
]
if image_config:
generation_config["image_config"] = image_config

View file

@ -126,6 +126,73 @@ class TestVertexAIGeminiImageEditTransformation:
"utf-8"
)
def test_transform_image_edit_request_with_image_size(self) -> None:
"""Test that imageSize is included in image_config"""
image_bytes = b"fake_image_data"
image = BytesIO(image_bytes)
optional_params = {
"imageSize": "2K",
}
request_body_str, files = self.config.transform_image_edit_request(
model=self.model,
prompt=self.prompt,
image=image,
image_edit_optional_request_params=optional_params,
litellm_params=MagicMock(),
headers={},
)
request_body = json.loads(request_body_str)
generation_config = request_body["generationConfig"]
assert "image_config" in generation_config
assert generation_config["image_config"]["image_size"] == "2K"
def test_transform_image_edit_request_with_aspect_ratio_and_image_size(self) -> None:
"""Test that both aspectRatio and imageSize are included in image_config"""
image_bytes = b"fake_image_data"
image = BytesIO(image_bytes)
optional_params = {
"aspectRatio": "16:9",
"imageSize": "2K",
}
request_body_str, files = self.config.transform_image_edit_request(
model=self.model,
prompt=self.prompt,
image=image,
image_edit_optional_request_params=optional_params,
litellm_params=MagicMock(),
headers={},
)
request_body = json.loads(request_body_str)
image_config = request_body["generationConfig"]["image_config"]
assert image_config["aspect_ratio"] == "16:9"
assert image_config["image_size"] == "2K"
def test_map_openai_params_size_as_resolution(self) -> None:
"""Test that size='2K' maps to imageSize instead of aspectRatio"""
optional_params: Dict[str, object] = {"size": "2K"}
mapped = self.config.map_openai_params(
image_edit_optional_params=optional_params, # type: ignore[arg-type]
model=self.model,
drop_params=False,
)
assert "imageSize" in mapped
assert mapped["imageSize"] == "2K"
assert "aspectRatio" not in mapped
def test_map_openai_params_quality_hd(self) -> None:
"""Test that quality='hd' maps to imageSize='2K'"""
optional_params: Dict[str, object] = {"quality": "hd"}
mapped = self.config.map_openai_params(
image_edit_optional_params=optional_params, # type: ignore[arg-type]
model=self.model,
drop_params=False,
)
assert mapped["imageSize"] == "2K"
def test_transform_image_edit_request_without_image_raises(self) -> None:
"""Test that missing image raises ValueError"""
optional_params = {}