codex-ai 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codex_ai-0.2.1 → codex_ai-0.2.3}/CHANGELOG.md +14 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/PKG-INFO +11 -7
- {codex_ai-0.2.1 → codex_ai-0.2.3}/README.md +7 -3
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/architecture/providers/README.md +2 -1
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/architecture/providers/data_flow.md +2 -1
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/ru/architecture/providers/README.md +2 -1
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/ru/architecture/providers/data_flow.md +2 -1
- {codex_ai-0.2.1 → codex_ai-0.2.3}/pyproject.toml +1 -1
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/core/dispatcher.py +2 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/core/protocol.py +3 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/providers/gemini.py +61 -14
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/core/test_dispatcher.py +6 -2
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/core/test_protocol.py +1 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/providers/test_gemini_provider.py +43 -1
- {codex_ai-0.2.1 → codex_ai-0.2.3}/uv.lock +6 -6
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.github/workflows/ci.yml +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.github/workflows/docs.yml +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.github/workflows/publish.yml +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.gitignore +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.nojekyll +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.pre-commit-config.yaml +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.python-version +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/.secrets.baseline +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/LICENSE +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/changelog.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/core/dispatcher.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/core/exceptions.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/core/protocol.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/core/router.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/core/sync.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/index.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/providers/gemini.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/api/providers/openai.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/architecture/core/README.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/en/architecture/core/data_flow.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/index.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/ru/architecture/core/README.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/ru/architecture/core/data_flow.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/docs/stylesheets/extra.css +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/mkdocs.yml +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/core/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/core/exceptions.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/core/router.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/core/sync.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/providers/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/providers/openai.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/src/codex_ai/py.typed +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/conftest.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/integration/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/integration/conftest.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/integration/test_providers_integration.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/conftest.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/core/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/core/test_exceptions.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/core/test_router.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/core/test_sync.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/providers/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/providers/test_openai_provider.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tests/unit/test_public_api.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tools/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tools/dev/README.md +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tools/dev/__init__.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tools/dev/check.py +0 -0
- {codex_ai-0.2.1 → codex_ai-0.2.3}/tools/dev/generate_project_tree.py +0 -0
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.3] - 2026-05-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added explicit Gemini image `image_config` support for aspect ratio and image size controls.
|
|
11
|
+
- Added a Gemini image retry from `image_size="4K"` to `image_size="2K"` when the initial 4K request is rejected.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Updated the Gemini extra to `google-genai==2.3.0` for the SDK image configuration contract.
|
|
15
|
+
|
|
16
|
+
## [0.2.2] - 2026-05-15
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Changed the Gemini image default model to the official API model id `gemini-2.5-flash-image`.
|
|
20
|
+
|
|
7
21
|
## [0.2.1] - 2026-05-15
|
|
8
22
|
|
|
9
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-ai
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Gemini-first and OpenAI provider helpers for Codex
|
|
5
5
|
Project-URL: Homepage, https://github.com/codexdlc/codex-ai
|
|
6
6
|
Project-URL: Documentation, https://codexdlc.github.io/codex-ai/
|
|
@@ -19,12 +19,12 @@ Requires-Python: >=3.12
|
|
|
19
19
|
Requires-Dist: codex-core<0.4.0,>=0.2.2
|
|
20
20
|
Requires-Dist: pydantic<3.0,>=2.0
|
|
21
21
|
Provides-Extra: all
|
|
22
|
-
Requires-Dist: google-genai==
|
|
22
|
+
Requires-Dist: google-genai==2.3.0; extra == 'all'
|
|
23
23
|
Requires-Dist: openai<2.0,>=1.0; extra == 'all'
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: bandit>=1.7; extra == 'dev'
|
|
26
26
|
Requires-Dist: detect-secrets>=1.5; extra == 'dev'
|
|
27
|
-
Requires-Dist: google-genai==
|
|
27
|
+
Requires-Dist: google-genai==2.3.0; extra == 'dev'
|
|
28
28
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
29
|
Requires-Dist: openai<2.0,>=1.0; extra == 'dev'
|
|
30
30
|
Requires-Dist: pip-audit>=2.7; extra == 'dev'
|
|
@@ -40,7 +40,7 @@ Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
|
|
|
40
40
|
Requires-Dist: mkdocs>=1.5; extra == 'docs'
|
|
41
41
|
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
42
42
|
Provides-Extra: gemini
|
|
43
|
-
Requires-Dist: google-genai==
|
|
43
|
+
Requires-Dist: google-genai==2.3.0; extra == 'gemini'
|
|
44
44
|
Provides-Extra: openai
|
|
45
45
|
Requires-Dist: openai<2.0,>=1.0; extra == 'openai'
|
|
46
46
|
Description-Content-Type: text/markdown
|
|
@@ -83,8 +83,10 @@ gemini = GeminiProvider(api_key="AIza...")
|
|
|
83
83
|
text = await gemini.generate_text("Write one short tavern rumor.")
|
|
84
84
|
loot = await gemini.generate_json("Create one loot item.", schema=LootItem)
|
|
85
85
|
image_bytes, content_type = await gemini.generate_image_bytes(
|
|
86
|
-
"
|
|
87
|
-
|
|
86
|
+
"Square tactical dark fantasy ruined capital city map, no labels.",
|
|
87
|
+
model="gemini-3-pro-image-preview",
|
|
88
|
+
response_mime_type="image/png",
|
|
89
|
+
image_config={"aspect_ratio": "1:1", "image_size": "4K"},
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
|
|
@@ -97,7 +99,9 @@ imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
|
|
|
97
99
|
|
|
98
100
|
`generate_image_bytes()` targets Gemini image models through `generate_content` and treats
|
|
99
101
|
`response_mime_type` as a preferred/fallback MIME type. It does not pass image MIME values
|
|
100
|
-
to Gemini's text `response_mime_type` config field.
|
|
102
|
+
to Gemini's text `response_mime_type` config field. Pass Gemini image controls such as
|
|
103
|
+
`aspect_ratio` and `image_size` with `image_config`; if a `4K` request is rejected,
|
|
104
|
+
the Gemini provider retries once with `2K`. Use `generate_imagen_bytes()` for
|
|
101
105
|
Imagen models; that path uses `generate_images` and passes the requested MIME as
|
|
102
106
|
`output_mime_type`.
|
|
103
107
|
|
|
@@ -36,8 +36,10 @@ gemini = GeminiProvider(api_key="AIza...")
|
|
|
36
36
|
text = await gemini.generate_text("Write one short tavern rumor.")
|
|
37
37
|
loot = await gemini.generate_json("Create one loot item.", schema=LootItem)
|
|
38
38
|
image_bytes, content_type = await gemini.generate_image_bytes(
|
|
39
|
-
"
|
|
40
|
-
|
|
39
|
+
"Square tactical dark fantasy ruined capital city map, no labels.",
|
|
40
|
+
model="gemini-3-pro-image-preview",
|
|
41
|
+
response_mime_type="image/png",
|
|
42
|
+
image_config={"aspect_ratio": "1:1", "image_size": "4K"},
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
|
|
@@ -50,7 +52,9 @@ imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
|
|
|
50
52
|
|
|
51
53
|
`generate_image_bytes()` targets Gemini image models through `generate_content` and treats
|
|
52
54
|
`response_mime_type` as a preferred/fallback MIME type. It does not pass image MIME values
|
|
53
|
-
to Gemini's text `response_mime_type` config field.
|
|
55
|
+
to Gemini's text `response_mime_type` config field. Pass Gemini image controls such as
|
|
56
|
+
`aspect_ratio` and `image_size` with `image_config`; if a `4K` request is rejected,
|
|
57
|
+
the Gemini provider retries once with `2K`. Use `generate_imagen_bytes()` for
|
|
54
58
|
Imagen models; that path uses `generate_images` and passes the requested MIME as
|
|
55
59
|
`output_mime_type`.
|
|
56
60
|
|
|
@@ -47,6 +47,7 @@ LLMRouter builder -> PromptResult -> LLMDispatcher.process() -> provider.answer(
|
|
|
47
47
|
- Gemini-specific capabilities are represented directly instead of being hidden behind a broad universal abstraction.
|
|
48
48
|
- JSON generation uses provider-native JSON configuration and still validates locally with `json.loads` and optional Pydantic models.
|
|
49
49
|
- Gemini image generation and Imagen generation are separate explicit methods because they use different SDK calls.
|
|
50
|
-
- `generate_image_bytes()` uses Gemini `generate_content` with image modality. Its `response_mime_type` is only a preferred/fallback MIME type
|
|
50
|
+
- `generate_image_bytes()` uses Gemini `generate_content` with image modality. Its `response_mime_type` is only a preferred/fallback MIME type, and Gemini image controls are passed through `image_config`.
|
|
51
|
+
- `generate_image_bytes()` retries a rejected `image_config={"image_size": "4K"}` request once as `2K`.
|
|
51
52
|
- `generate_imagen_bytes()` uses Imagen `generate_images` and passes the requested MIME as `output_mime_type`.
|
|
52
53
|
- Anthropic, OpenRouter, and multi-provider failover are not active APIs in this alpha line.
|
|
@@ -28,12 +28,13 @@ PromptResult | str
|
|
|
28
28
|
```
|
|
29
29
|
prompt: str
|
|
30
30
|
-> GeminiProvider.generate_image_bytes()
|
|
31
|
-
-> GenerateContentConfig(response_modalities=[IMAGE])
|
|
31
|
+
-> GenerateContentConfig(response_modalities=[IMAGE], image_config=...)
|
|
32
32
|
-> first inline_data image part
|
|
33
33
|
-> (bytes, actual_mime_type)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`response_mime_type` is not passed to `GenerateContentConfig.response_mime_type` on this path; it is only a fallback content type when Gemini omits `inline_data.mime_type`.
|
|
37
|
+
When `image_config.image_size` is `4K` and Gemini rejects the request, the provider retries once with `image_size` changed to `2K`.
|
|
37
38
|
|
|
38
39
|
## Imagen Images
|
|
39
40
|
|
|
@@ -47,6 +47,7 @@ LLMRouter builder -> PromptResult -> LLMDispatcher.process() -> provider.answer(
|
|
|
47
47
|
- Возможности Gemini представлены напрямую, без широкой универсальной абстракции.
|
|
48
48
|
- JSON generation использует native JSON config провайдера и локальную проверку через `json.loads` и optional Pydantic schema.
|
|
49
49
|
- Gemini image generation и Imagen generation разведены в отдельные явные методы, потому что они используют разные SDK calls.
|
|
50
|
-
- `generate_image_bytes()` использует Gemini `generate_content` с image modality. Его `response_mime_type` является только preferred/fallback MIME type
|
|
50
|
+
- `generate_image_bytes()` использует Gemini `generate_content` с image modality. Его `response_mime_type` является только preferred/fallback MIME type, а Gemini image controls передаются через `image_config`.
|
|
51
|
+
- `generate_image_bytes()` один раз повторяет отклоненный `image_config={"image_size": "4K"}` запрос как `2K`.
|
|
51
52
|
- `generate_imagen_bytes()` использует Imagen `generate_images` и передает requested MIME как `output_mime_type`.
|
|
52
53
|
- Anthropic, OpenRouter и multi-provider failover не являются активными API в этой alpha-линейке.
|
|
@@ -28,12 +28,13 @@ PromptResult | str
|
|
|
28
28
|
```
|
|
29
29
|
prompt: str
|
|
30
30
|
-> GeminiProvider.generate_image_bytes()
|
|
31
|
-
-> GenerateContentConfig(response_modalities=[IMAGE])
|
|
31
|
+
-> GenerateContentConfig(response_modalities=[IMAGE], image_config=...)
|
|
32
32
|
-> first inline_data image part
|
|
33
33
|
-> (bytes, actual_mime_type)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`response_mime_type` в этом пути не передается в `GenerateContentConfig.response_mime_type`; он используется только как fallback content type, если Gemini не вернул `inline_data.mime_type`.
|
|
37
|
+
Если `image_config.image_size` равен `4K` и Gemini отклоняет запрос, провайдер один раз повторяет его с `image_size="2K"`.
|
|
37
38
|
|
|
38
39
|
## Imagen Images
|
|
39
40
|
|
|
@@ -96,6 +96,7 @@ class LLMDispatcher:
|
|
|
96
96
|
*,
|
|
97
97
|
model: str | None = None,
|
|
98
98
|
response_mime_type: str = "image/webp",
|
|
99
|
+
image_config: dict[str, Any] | None = None,
|
|
99
100
|
**kwargs: Any,
|
|
100
101
|
) -> tuple[bytes, str]:
|
|
101
102
|
"""
|
|
@@ -114,6 +115,7 @@ class LLMDispatcher:
|
|
|
114
115
|
prompt,
|
|
115
116
|
model=model,
|
|
116
117
|
response_mime_type=response_mime_type,
|
|
118
|
+
image_config=image_config,
|
|
117
119
|
**kwargs,
|
|
118
120
|
)
|
|
119
121
|
|
|
@@ -148,6 +148,7 @@ class ImageGenerationProvider(Protocol):
|
|
|
148
148
|
*,
|
|
149
149
|
model: str | None = None,
|
|
150
150
|
response_mime_type: str = "image/webp",
|
|
151
|
+
image_config: dict[str, Any] | None = None,
|
|
151
152
|
**kwargs: Any,
|
|
152
153
|
) -> tuple[bytes, str]:
|
|
153
154
|
"""
|
|
@@ -157,6 +158,8 @@ class ImageGenerationProvider(Protocol):
|
|
|
157
158
|
prompt: Plain image-generation prompt.
|
|
158
159
|
model: Optional image model override.
|
|
159
160
|
response_mime_type: Requested/preferred image MIME type.
|
|
161
|
+
image_config: Optional image generation controls such as
|
|
162
|
+
``{"aspect_ratio": "1:1", "image_size": "4K"}``.
|
|
160
163
|
**kwargs: Extra provider-specific kwargs.
|
|
161
164
|
|
|
162
165
|
Returns:
|
|
@@ -28,7 +28,7 @@ from codex_ai.core.exceptions import LLMProviderError
|
|
|
28
28
|
from codex_ai.core.protocol import PromptResult
|
|
29
29
|
|
|
30
30
|
_DEFAULT_MODEL = "gemini-2.5-flash-lite"
|
|
31
|
-
_DEFAULT_IMAGE_MODEL = "gemini-
|
|
31
|
+
_DEFAULT_IMAGE_MODEL = "gemini-2.5-flash-image"
|
|
32
32
|
_DEFAULT_IMAGEN_MODEL = "imagen-3.0-generate-002"
|
|
33
33
|
|
|
34
34
|
|
|
@@ -41,7 +41,7 @@ class GeminiProvider:
|
|
|
41
41
|
Args:
|
|
42
42
|
api_key: Google AI API key.
|
|
43
43
|
model: Gemini text model name. Defaults to ``"gemini-2.5-flash-lite"``.
|
|
44
|
-
image_model: Gemini image model name. Defaults to ``"gemini-
|
|
44
|
+
image_model: Gemini image model name. Defaults to ``"gemini-2.5-flash-image"``.
|
|
45
45
|
|
|
46
46
|
Example:
|
|
47
47
|
```python
|
|
@@ -186,6 +186,7 @@ class GeminiProvider:
|
|
|
186
186
|
*,
|
|
187
187
|
model: str | None = None,
|
|
188
188
|
response_mime_type: str = "image/webp",
|
|
189
|
+
image_config: dict[str, Any] | None = None,
|
|
189
190
|
**kwargs: Any,
|
|
190
191
|
) -> tuple[bytes, str]:
|
|
191
192
|
"""
|
|
@@ -206,29 +207,75 @@ class GeminiProvider:
|
|
|
206
207
|
runtime_kw = kwargs.copy()
|
|
207
208
|
runtime_kw.pop("model", None)
|
|
208
209
|
runtime_kw.pop("response_mime_type", None)
|
|
210
|
+
runtime_kw.pop("image_config", None)
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
response_modalities
|
|
212
|
+
config_kwargs: dict[str, Any] = {
|
|
213
|
+
"response_modalities": [genai_types.Modality.IMAGE],
|
|
212
214
|
**runtime_kw,
|
|
213
|
-
|
|
215
|
+
}
|
|
216
|
+
if image_config is not None:
|
|
217
|
+
config_kwargs["image_config"] = image_config
|
|
214
218
|
|
|
215
219
|
try:
|
|
216
|
-
|
|
220
|
+
return await self._generate_image_once(
|
|
217
221
|
model=selected_model,
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
prompt=prompt,
|
|
223
|
+
config_kwargs=config_kwargs,
|
|
224
|
+
fallback_mime=requested_mime,
|
|
220
225
|
)
|
|
221
|
-
image = self._extract_first_inline_image(response, fallback_mime=requested_mime)
|
|
222
|
-
if image is not None:
|
|
223
|
-
return image
|
|
224
|
-
|
|
225
|
-
detail = self._describe_non_image_response(response)
|
|
226
|
-
raise LLMProviderError(f"Gemini image generation did not return image data{detail}")
|
|
227
226
|
except LLMProviderError:
|
|
228
227
|
raise
|
|
229
228
|
except Exception as exc:
|
|
229
|
+
fallback_config_kwargs = self._fallback_4k_image_config_to_2k(config_kwargs)
|
|
230
|
+
if fallback_config_kwargs is not None:
|
|
231
|
+
try:
|
|
232
|
+
return await self._generate_image_once(
|
|
233
|
+
model=selected_model,
|
|
234
|
+
prompt=prompt,
|
|
235
|
+
config_kwargs=fallback_config_kwargs,
|
|
236
|
+
fallback_mime=requested_mime,
|
|
237
|
+
)
|
|
238
|
+
except LLMProviderError:
|
|
239
|
+
raise
|
|
240
|
+
except Exception as fallback_exc:
|
|
241
|
+
raise LLMProviderError(f"Gemini image generation error: {fallback_exc}") from fallback_exc
|
|
242
|
+
|
|
230
243
|
raise LLMProviderError(f"Gemini image generation error: {exc}") from exc
|
|
231
244
|
|
|
245
|
+
async def _generate_image_once(
|
|
246
|
+
self,
|
|
247
|
+
*,
|
|
248
|
+
model: str,
|
|
249
|
+
prompt: str,
|
|
250
|
+
config_kwargs: dict[str, Any],
|
|
251
|
+
fallback_mime: str,
|
|
252
|
+
) -> tuple[bytes, str]:
|
|
253
|
+
config = genai_types.GenerateContentConfig(**config_kwargs)
|
|
254
|
+
response = await self._client.aio.models.generate_content(
|
|
255
|
+
model=model,
|
|
256
|
+
contents=prompt,
|
|
257
|
+
config=config,
|
|
258
|
+
)
|
|
259
|
+
image = self._extract_first_inline_image(response, fallback_mime=fallback_mime)
|
|
260
|
+
if image is not None:
|
|
261
|
+
return image
|
|
262
|
+
|
|
263
|
+
detail = self._describe_non_image_response(response)
|
|
264
|
+
raise LLMProviderError(f"Gemini image generation did not return image data{detail}")
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _fallback_4k_image_config_to_2k(config_kwargs: dict[str, Any]) -> dict[str, Any] | None:
|
|
268
|
+
image_config = config_kwargs.get("image_config")
|
|
269
|
+
if not isinstance(image_config, dict) or image_config.get("image_size") != "4K":
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
fallback_image_config = image_config.copy()
|
|
273
|
+
fallback_image_config["image_size"] = "2K"
|
|
274
|
+
|
|
275
|
+
fallback_config_kwargs = config_kwargs.copy()
|
|
276
|
+
fallback_config_kwargs["image_config"] = fallback_image_config
|
|
277
|
+
return fallback_config_kwargs
|
|
278
|
+
|
|
232
279
|
async def generate_imagen_bytes(
|
|
233
280
|
self,
|
|
234
281
|
prompt: str,
|
|
@@ -126,9 +126,10 @@ class ImageProvider:
|
|
|
126
126
|
*,
|
|
127
127
|
model: str | None = None,
|
|
128
128
|
response_mime_type: str = "image/webp",
|
|
129
|
+
image_config: dict | None = None,
|
|
129
130
|
**kwargs,
|
|
130
131
|
) -> tuple[bytes, str]:
|
|
131
|
-
self.calls.append((prompt, model, response_mime_type, kwargs))
|
|
132
|
+
self.calls.append((prompt, model, response_mime_type, image_config, kwargs))
|
|
132
133
|
return b"image-bytes", "image/png"
|
|
133
134
|
|
|
134
135
|
async def generate_imagen_bytes(
|
|
@@ -159,11 +160,14 @@ async def test_dispatcher_generate_image_bytes_delegates_to_image_provider():
|
|
|
159
160
|
"draw a castle",
|
|
160
161
|
model="gemini-image",
|
|
161
162
|
response_mime_type="image/webp",
|
|
163
|
+
image_config={"aspect_ratio": "1:1", "image_size": "4K"},
|
|
162
164
|
seed=123,
|
|
163
165
|
)
|
|
164
166
|
|
|
165
167
|
assert result == (b"image-bytes", "image/png")
|
|
166
|
-
assert provider.calls == [
|
|
168
|
+
assert provider.calls == [
|
|
169
|
+
("draw a castle", "gemini-image", "image/webp", {"aspect_ratio": "1:1", "image_size": "4K"}, {"seed": 123})
|
|
170
|
+
]
|
|
167
171
|
|
|
168
172
|
|
|
169
173
|
async def test_dispatcher_generate_image_bytes_raises_for_unsupported_provider(mock_provider):
|
|
@@ -106,6 +106,7 @@ def test_image_generation_provider_structural_check():
|
|
|
106
106
|
*,
|
|
107
107
|
model: str | None = None,
|
|
108
108
|
response_mime_type: str = "image/webp",
|
|
109
|
+
image_config: dict | None = None,
|
|
109
110
|
**kwargs,
|
|
110
111
|
) -> tuple[bytes, str]:
|
|
111
112
|
return b"image", response_mime_type
|
|
@@ -384,6 +384,19 @@ async def test_gemini_generate_image_bytes_uses_image_model_not_text_model():
|
|
|
384
384
|
assert kwargs["contents"] == "draw a castle"
|
|
385
385
|
|
|
386
386
|
|
|
387
|
+
async def test_gemini_generate_image_bytes_default_image_model_matches_api_id():
|
|
388
|
+
provider, mock_generate, _ = _make_provider()
|
|
389
|
+
mock_generate.return_value = _image_response()
|
|
390
|
+
|
|
391
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
392
|
+
mock_types.Modality.IMAGE = "IMAGE"
|
|
393
|
+
mock_types.GenerateContentConfig.return_value = MagicMock()
|
|
394
|
+
await provider.generate_image_bytes("draw a castle")
|
|
395
|
+
|
|
396
|
+
_, kwargs = mock_generate.call_args
|
|
397
|
+
assert kwargs["model"] == "gemini-2.5-flash-image"
|
|
398
|
+
|
|
399
|
+
|
|
387
400
|
async def test_gemini_generate_image_bytes_model_override_wins():
|
|
388
401
|
provider, mock_generate, _ = _make_provider()
|
|
389
402
|
provider._image_model = "image-model"
|
|
@@ -405,14 +418,43 @@ async def test_gemini_generate_image_bytes_config_requests_image_modality_not_te
|
|
|
405
418
|
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
406
419
|
mock_types.Modality.IMAGE = "IMAGE"
|
|
407
420
|
mock_types.GenerateContentConfig.return_value = MagicMock()
|
|
408
|
-
await provider.generate_image_bytes(
|
|
421
|
+
await provider.generate_image_bytes(
|
|
422
|
+
"draw a castle",
|
|
423
|
+
response_mime_type="image/png",
|
|
424
|
+
image_config={"aspect_ratio": "1:1", "image_size": "4K"},
|
|
425
|
+
seed=7,
|
|
426
|
+
)
|
|
409
427
|
|
|
410
428
|
config_kwargs = mock_types.GenerateContentConfig.call_args.kwargs
|
|
411
429
|
assert config_kwargs["response_modalities"] == ["IMAGE"]
|
|
412
430
|
assert "response_mime_type" not in config_kwargs
|
|
431
|
+
assert config_kwargs["image_config"] == {"aspect_ratio": "1:1", "image_size": "4K"}
|
|
413
432
|
assert config_kwargs["seed"] == 7
|
|
414
433
|
|
|
415
434
|
|
|
435
|
+
async def test_gemini_generate_image_bytes_falls_back_from_4k_to_2k_when_config_rejected():
|
|
436
|
+
provider, mock_generate, _ = _make_provider()
|
|
437
|
+
mock_generate.side_effect = [
|
|
438
|
+
ValueError("unsupported image_size"),
|
|
439
|
+
_image_response(data=b"png", mime_type="image/png"),
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
443
|
+
mock_types.Modality.IMAGE = "IMAGE"
|
|
444
|
+
mock_types.GenerateContentConfig.side_effect = lambda **kwargs: kwargs
|
|
445
|
+
result = await provider.generate_image_bytes(
|
|
446
|
+
"draw a castle",
|
|
447
|
+
response_mime_type="image/png",
|
|
448
|
+
image_config={"aspect_ratio": "1:1", "image_size": "4K"},
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
assert result == (b"png", "image/png")
|
|
452
|
+
first_config = mock_generate.call_args_list[0].kwargs["config"]
|
|
453
|
+
second_config = mock_generate.call_args_list[1].kwargs["config"]
|
|
454
|
+
assert first_config["image_config"] == {"aspect_ratio": "1:1", "image_size": "4K"}
|
|
455
|
+
assert second_config["image_config"] == {"aspect_ratio": "1:1", "image_size": "2K"}
|
|
456
|
+
|
|
457
|
+
|
|
416
458
|
async def test_gemini_generate_image_bytes_returns_inline_image_bytes_and_actual_mime():
|
|
417
459
|
provider, mock_generate, _ = _make_provider()
|
|
418
460
|
mock_generate.return_value = _image_response(data=b"png-bytes", mime_type="image/png")
|
|
@@ -344,9 +344,9 @@ requires-dist = [
|
|
|
344
344
|
{ name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7" },
|
|
345
345
|
{ name = "codex-core", specifier = ">=0.2.2,<0.4.0" },
|
|
346
346
|
{ name = "detect-secrets", marker = "extra == 'dev'", specifier = ">=1.5" },
|
|
347
|
-
{ name = "google-genai", marker = "extra == 'all'", specifier = "==
|
|
348
|
-
{ name = "google-genai", marker = "extra == 'dev'", specifier = "==
|
|
349
|
-
{ name = "google-genai", marker = "extra == 'gemini'", specifier = "==
|
|
347
|
+
{ name = "google-genai", marker = "extra == 'all'", specifier = "==2.3.0" },
|
|
348
|
+
{ name = "google-genai", marker = "extra == 'dev'", specifier = "==2.3.0" },
|
|
349
|
+
{ name = "google-genai", marker = "extra == 'gemini'", specifier = "==2.3.0" },
|
|
350
350
|
{ name = "mike", marker = "extra == 'docs'", specifier = ">=2.0" },
|
|
351
351
|
{ name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.5" },
|
|
352
352
|
{ name = "mkdocs-include-markdown-plugin", marker = "extra == 'docs'" },
|
|
@@ -622,7 +622,7 @@ requests = [
|
|
|
622
622
|
|
|
623
623
|
[[package]]
|
|
624
624
|
name = "google-genai"
|
|
625
|
-
version = "
|
|
625
|
+
version = "2.3.0"
|
|
626
626
|
source = { registry = "https://pypi.org/simple" }
|
|
627
627
|
dependencies = [
|
|
628
628
|
{ name = "anyio" },
|
|
@@ -636,9 +636,9 @@ dependencies = [
|
|
|
636
636
|
{ name = "typing-extensions" },
|
|
637
637
|
{ name = "websockets" },
|
|
638
638
|
]
|
|
639
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
639
|
+
sdist = { url = "https://files.pythonhosted.org/packages/02/8e/dfa4b34dd4c0baffccf6466fc68d6d35011662d43e7d79accb902320db74/google_genai-2.3.0.tar.gz", hash = "sha256:e877c750a4ccacdd9928fc3aa8ca8820ce85cade0ca51bd83feceacf5959b579", size = 546930, upload-time = "2026-05-15T06:22:36.264Z" }
|
|
640
640
|
wheels = [
|
|
641
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
641
|
+
{ url = "https://files.pythonhosted.org/packages/b4/6e/aa6b30b09f58b946750fc4089c5248fbd3576f746e0e818d88633559dc84/google_genai-2.3.0-py3-none-any.whl", hash = "sha256:89d3c71c9f5f5b931b405b88a5837aea2bd4d27ed90323b9599f5760bbb91d92", size = 805484, upload-time = "2026-05-15T06:22:34.247Z" },
|
|
642
642
|
]
|
|
643
643
|
|
|
644
644
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|