fix(image): pass aspect ratio and size to Gemini Flash image models
The Gemini Flash image path (`generateContent`) dropped both `aspect_ratio`
and `image_size`: `generate()` never forwarded them and
`_generate_gemini_flash` did not accept them, so every request fell back to
1:1 / input-matched output. The Imagen path was unaffected.
Forward the hints and emit them under
`generationConfig.responseFormat.image` per the current Gemini API. Aspect
ratio is validated against the accepted set; `imageSize` is validated against
{512,1K,2K,4K} and only sent to Gemini 3+ image models, since
`gemini-2.5-flash-image` supports only `aspectRatio`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Claude Opus 4.8
parent
b695a7e875
commit
4986590bd7
@@ -33,6 +33,13 @@ _AIHUBMIX_ASPECT_RATIO_SIZES = {
|
||||
}
|
||||
_GEMINI_DEFAULT_TIMEOUT_S = 120.0
|
||||
_GEMINI_IMAGEN_ASPECT_RATIOS = {"1:1", "9:16", "16:9", "3:4", "4:3"}
|
||||
# Aspect ratios accepted by the Gemini Flash image (generateContent) models.
|
||||
_GEMINI_FLASH_ASPECT_RATIOS = {
|
||||
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4",
|
||||
"9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1",
|
||||
}
|
||||
# Image-size tokens accepted by Gemini 3+ image models (2.5 Flash Image ignores it).
|
||||
_GEMINI_FLASH_IMAGE_SIZES = {"512", "1K", "2K", "4K"}
|
||||
_OLLAMA_DEFAULT_SIDE = 1024
|
||||
_OLLAMA_SIZE_PRESETS = {
|
||||
"1K": 1024,
|
||||
@@ -635,7 +642,11 @@ class GeminiImageGenerationClient(ImageGenerationProvider):
|
||||
prompt=prompt, model=model, aspect_ratio=aspect_ratio
|
||||
)
|
||||
return await self._generate_gemini_flash(
|
||||
prompt=prompt, model=model, reference_images=reference_images or []
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
reference_images=reference_images or [],
|
||||
aspect_ratio=aspect_ratio,
|
||||
image_size=image_size,
|
||||
)
|
||||
|
||||
async def _generate_imagen(
|
||||
@@ -691,15 +702,22 @@ class GeminiImageGenerationClient(ImageGenerationProvider):
|
||||
prompt: str,
|
||||
model: str,
|
||||
reference_images: list[str],
|
||||
aspect_ratio: str | None = None,
|
||||
image_size: str | None = None,
|
||||
) -> GeneratedImageResponse:
|
||||
parts: list[dict[str, Any]] = [
|
||||
{"inlineData": image_path_to_inline_data(path)} for path in reference_images
|
||||
]
|
||||
parts.append({"text": prompt})
|
||||
|
||||
generation_config: dict[str, Any] = {"responseModalities": ["TEXT", "IMAGE"]}
|
||||
image_config = _gemini_flash_image_config(model, aspect_ratio, image_size)
|
||||
if image_config:
|
||||
generation_config["responseFormat"] = {"image": image_config}
|
||||
|
||||
body: dict[str, Any] = {
|
||||
"contents": [{"role": "user", "parts": parts}],
|
||||
"generationConfig": {"responseModalities": ["TEXT", "IMAGE"]},
|
||||
"generationConfig": generation_config,
|
||||
}
|
||||
body.update(self.extra_body)
|
||||
|
||||
@@ -748,6 +766,31 @@ class GeminiImageGenerationClient(ImageGenerationProvider):
|
||||
)
|
||||
|
||||
|
||||
def _gemini_flash_image_config(
|
||||
model: str,
|
||||
aspect_ratio: str | None,
|
||||
image_size: str | None,
|
||||
) -> dict[str, str]:
|
||||
"""Build the ``responseFormat.image`` config for Gemini Flash image models.
|
||||
|
||||
Aspect ratio applies to all Flash image models; image size is only honored
|
||||
by Gemini 3+ image models (``gemini-2.5-flash-image`` ignores it).
|
||||
"""
|
||||
config: dict[str, str] = {}
|
||||
if aspect_ratio and aspect_ratio in _GEMINI_FLASH_ASPECT_RATIOS:
|
||||
config["aspectRatio"] = aspect_ratio
|
||||
if image_size and _gemini_flash_supports_image_size(model):
|
||||
normalized = image_size.strip().upper()
|
||||
if normalized in _GEMINI_FLASH_IMAGE_SIZES:
|
||||
config["imageSize"] = normalized
|
||||
return config
|
||||
|
||||
|
||||
def _gemini_flash_supports_image_size(model: str) -> bool:
|
||||
"""Return whether the model honors ``imageSize`` (Gemini 3+ image models)."""
|
||||
return "2.5" not in model.lower()
|
||||
|
||||
|
||||
async def _aihubmix_images_from_payload(
|
||||
client: httpx.AsyncClient,
|
||||
payload: dict[str, Any],
|
||||
|
||||
@@ -422,6 +422,63 @@ async def test_gemini_flash_reference_images(tmp_path: Path) -> None:
|
||||
assert parts[1] == {"text": "edit this"}
|
||||
|
||||
|
||||
def _gemini_flash_image_response() -> FakeResponse:
|
||||
return FakeResponse(
|
||||
{
|
||||
"candidates": [
|
||||
{"content": {"parts": [{"inlineData": {"mimeType": "image/png", "data": RAW_B64}}]}}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_flash_forwards_aspect_ratio_and_image_size() -> None:
|
||||
fake = FakeClient(_gemini_flash_image_response())
|
||||
client = GeminiImageGenerationClient(api_key="AIza-test", client=fake) # type: ignore[arg-type]
|
||||
|
||||
await client.generate(
|
||||
prompt="draw a cat",
|
||||
model="gemini-3-pro-image",
|
||||
aspect_ratio="16:9",
|
||||
image_size="2K",
|
||||
)
|
||||
|
||||
image_config = fake.calls[0]["json"]["generationConfig"]["responseFormat"]["image"]
|
||||
assert image_config == {"aspectRatio": "16:9", "imageSize": "2K"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_flash_2_5_drops_image_size() -> None:
|
||||
fake = FakeClient(_gemini_flash_image_response())
|
||||
client = GeminiImageGenerationClient(api_key="AIza-test", client=fake) # type: ignore[arg-type]
|
||||
|
||||
await client.generate(
|
||||
prompt="draw a cat",
|
||||
model="gemini-2.5-flash-image",
|
||||
aspect_ratio="4:3",
|
||||
image_size="1K",
|
||||
)
|
||||
|
||||
image_config = fake.calls[0]["json"]["generationConfig"]["responseFormat"]["image"]
|
||||
assert image_config == {"aspectRatio": "4:3"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_flash_ignores_unsupported_hints() -> None:
|
||||
fake = FakeClient(_gemini_flash_image_response())
|
||||
client = GeminiImageGenerationClient(api_key="AIza-test", client=fake) # type: ignore[arg-type]
|
||||
|
||||
await client.generate(
|
||||
prompt="draw a cat",
|
||||
model="gemini-3-pro-image",
|
||||
aspect_ratio="7:5",
|
||||
image_size="1024x1024",
|
||||
)
|
||||
|
||||
assert "responseFormat" not in fake.calls[0]["json"]["generationConfig"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_requires_api_key() -> None:
|
||||
client = GeminiImageGenerationClient(api_key=None)
|
||||
|
||||
Reference in New Issue
Block a user