codex-ai 0.2.0__tar.gz → 0.2.2__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.0 → codex_ai-0.2.2}/CHANGELOG.md +15 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/PKG-INFO +17 -5
- {codex_ai-0.2.0 → codex_ai-0.2.2}/README.md +13 -1
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/providers/README.md +5 -1
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/providers/data_flow.md +12 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/providers/README.md +5 -1
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/providers/data_flow.md +12 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/pyproject.toml +1 -1
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/__init__.py +2 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/__init__.py +2 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/dispatcher.py +28 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/protocol.py +33 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/providers/gemini.py +106 -7
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_dispatcher.py +38 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_protocol.py +20 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/providers/test_gemini_provider.py +136 -2
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/test_public_api.py +1 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/uv.lock +6 -6
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.github/workflows/ci.yml +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.github/workflows/docs.yml +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.github/workflows/publish.yml +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.gitignore +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.nojekyll +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.pre-commit-config.yaml +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.python-version +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/.secrets.baseline +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/LICENSE +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/changelog.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/dispatcher.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/exceptions.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/protocol.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/router.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/sync.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/index.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/providers/gemini.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/providers/openai.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/core/README.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/core/data_flow.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/index.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/core/README.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/core/data_flow.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/stylesheets/extra.css +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/mkdocs.yml +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/exceptions.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/router.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/sync.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/providers/__init__.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/providers/openai.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/py.typed +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/conftest.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/integration/__init__.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/integration/conftest.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/integration/test_providers_integration.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/__init__.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/conftest.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/__init__.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_exceptions.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_router.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_sync.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/providers/__init__.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/providers/test_openai_provider.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/__init__.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/dev/README.md +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/dev/__init__.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/dev/check.py +0 -0
- {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/dev/generate_project_tree.py +0 -0
|
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.2] - 2026-05-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Changed the Gemini image default model to the official API model id `gemini-2.5-flash-image`.
|
|
11
|
+
|
|
12
|
+
## [0.2.1] - 2026-05-15
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Added explicit `GeminiProvider.generate_imagen_bytes()` for Imagen models through `generate_images`.
|
|
16
|
+
- Added `ImagenGenerationProvider` and dispatcher delegation for explicit Imagen generation.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Stopped passing image MIME values such as `image/webp` into Gemini `GenerateContentConfig.response_mime_type` on the Gemini image path.
|
|
20
|
+
- Pinned `google-genai` to `1.68.0` to avoid accidental SDK API drift in the alpha image-generation contract.
|
|
21
|
+
|
|
7
22
|
## [0.2.0] - 2026-05-15
|
|
8
23
|
|
|
9
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-ai
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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==1.68.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==1.68.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==1.68.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
|
|
@@ -84,12 +84,24 @@ 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
86
|
"A fantasy clan banner, game icon style.",
|
|
87
|
+
model="gemini-2.5-flash-image",
|
|
87
88
|
response_mime_type="image/webp",
|
|
88
89
|
)
|
|
90
|
+
|
|
91
|
+
imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
|
|
92
|
+
"A fantasy clan banner, game icon style.",
|
|
93
|
+
response_mime_type="image/jpeg",
|
|
94
|
+
)
|
|
89
95
|
```
|
|
90
96
|
|
|
91
97
|
`answer(prompt)` remains available as a compatibility wrapper for text generation.
|
|
92
98
|
|
|
99
|
+
`generate_image_bytes()` targets Gemini image models through `generate_content` and treats
|
|
100
|
+
`response_mime_type` as a preferred/fallback MIME type. It does not pass image MIME values
|
|
101
|
+
to Gemini's text `response_mime_type` config field. Use `generate_imagen_bytes()` for
|
|
102
|
+
Imagen models; that path uses `generate_images` and passes the requested MIME as
|
|
103
|
+
`output_mime_type`.
|
|
104
|
+
|
|
93
105
|
## Router Pipeline
|
|
94
106
|
|
|
95
107
|
```python
|
|
@@ -117,7 +129,7 @@ response = await dispatcher.process("chat", text="Hello!")
|
|
|
117
129
|
| Module | Extra | Description |
|
|
118
130
|
| :--- | :--- | :--- |
|
|
119
131
|
| `codex_ai.core` | - | Dispatcher, router, protocol types, sync wrapper, and shared exception contract |
|
|
120
|
-
| `codex_ai.providers.gemini` | `[gemini]` | Google Gemini text, JSON,
|
|
132
|
+
| `codex_ai.providers.gemini` | `[gemini]` | Google Gemini text, JSON, Gemini image, and Imagen generation via pinned `google-genai` |
|
|
121
133
|
| `codex_ai.providers.openai` | `[openai]` | OpenAI Chat Completions text provider |
|
|
122
134
|
|
|
123
135
|
## Development
|
|
@@ -37,12 +37,24 @@ 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
39
|
"A fantasy clan banner, game icon style.",
|
|
40
|
+
model="gemini-2.5-flash-image",
|
|
40
41
|
response_mime_type="image/webp",
|
|
41
42
|
)
|
|
43
|
+
|
|
44
|
+
imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
|
|
45
|
+
"A fantasy clan banner, game icon style.",
|
|
46
|
+
response_mime_type="image/jpeg",
|
|
47
|
+
)
|
|
42
48
|
```
|
|
43
49
|
|
|
44
50
|
`answer(prompt)` remains available as a compatibility wrapper for text generation.
|
|
45
51
|
|
|
52
|
+
`generate_image_bytes()` targets Gemini image models through `generate_content` and treats
|
|
53
|
+
`response_mime_type` as a preferred/fallback MIME type. It does not pass image MIME values
|
|
54
|
+
to Gemini's text `response_mime_type` config field. Use `generate_imagen_bytes()` for
|
|
55
|
+
Imagen models; that path uses `generate_images` and passes the requested MIME as
|
|
56
|
+
`output_mime_type`.
|
|
57
|
+
|
|
46
58
|
## Router Pipeline
|
|
47
59
|
|
|
48
60
|
```python
|
|
@@ -70,7 +82,7 @@ response = await dispatcher.process("chat", text="Hello!")
|
|
|
70
82
|
| Module | Extra | Description |
|
|
71
83
|
| :--- | :--- | :--- |
|
|
72
84
|
| `codex_ai.core` | - | Dispatcher, router, protocol types, sync wrapper, and shared exception contract |
|
|
73
|
-
| `codex_ai.providers.gemini` | `[gemini]` | Google Gemini text, JSON,
|
|
85
|
+
| `codex_ai.providers.gemini` | `[gemini]` | Google Gemini text, JSON, Gemini image, and Imagen generation via pinned `google-genai` |
|
|
74
86
|
| `codex_ai.providers.openai` | `[openai]` | OpenAI Chat Completions text provider |
|
|
75
87
|
|
|
76
88
|
## Development
|
|
@@ -10,6 +10,7 @@ Gemini is the primary target and exposes direct methods for text, JSON, and imag
|
|
|
10
10
|
await gemini.generate_text(...)
|
|
11
11
|
await gemini.generate_json(...)
|
|
12
12
|
await gemini.generate_image_bytes(...)
|
|
13
|
+
await gemini.generate_imagen_bytes(...)
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
OpenAI is kept as a text provider with the same `generate_text(...)` convenience.
|
|
@@ -22,6 +23,7 @@ PromptResult/String
|
|
|
22
23
|
├── GeminiProvider.generate_text(...) -> str
|
|
23
24
|
├── GeminiProvider.generate_json(...) -> dict | BaseModel
|
|
24
25
|
├── GeminiProvider.generate_image_bytes(...) -> tuple[bytes, str]
|
|
26
|
+
├── GeminiProvider.generate_imagen_bytes(...) -> tuple[bytes, str]
|
|
25
27
|
└── OpenAIProvider.generate_text(...) -> str
|
|
26
28
|
```
|
|
27
29
|
|
|
@@ -44,5 +46,7 @@ LLMRouter builder -> PromptResult -> LLMDispatcher.process() -> provider.answer(
|
|
|
44
46
|
|
|
45
47
|
- Gemini-specific capabilities are represented directly instead of being hidden behind a broad universal abstraction.
|
|
46
48
|
- JSON generation uses provider-native JSON configuration and still validates locally with `json.loads` and optional Pydantic models.
|
|
47
|
-
-
|
|
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.
|
|
51
|
+
- `generate_imagen_bytes()` uses Imagen `generate_images` and passes the requested MIME as `output_mime_type`.
|
|
48
52
|
- Anthropic, OpenRouter, and multi-provider failover are not active APIs in this alpha line.
|
|
@@ -33,6 +33,18 @@ prompt: str
|
|
|
33
33
|
-> (bytes, actual_mime_type)
|
|
34
34
|
```
|
|
35
35
|
|
|
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
|
+
|
|
38
|
+
## Imagen Images
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
prompt: str
|
|
42
|
+
-> GeminiProvider.generate_imagen_bytes()
|
|
43
|
+
-> GenerateImagesConfig(output_mime_type=requested_mime)
|
|
44
|
+
-> first generated_images image
|
|
45
|
+
-> (bytes, actual_mime_type)
|
|
46
|
+
```
|
|
47
|
+
|
|
36
48
|
## OpenAI Text
|
|
37
49
|
|
|
38
50
|
```
|
|
@@ -10,6 +10,7 @@ Gemini является основным направлением и дает п
|
|
|
10
10
|
await gemini.generate_text(...)
|
|
11
11
|
await gemini.generate_json(...)
|
|
12
12
|
await gemini.generate_image_bytes(...)
|
|
13
|
+
await gemini.generate_imagen_bytes(...)
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
OpenAI оставлен как текстовый провайдер с `generate_text(...)`.
|
|
@@ -22,6 +23,7 @@ PromptResult/String
|
|
|
22
23
|
├── GeminiProvider.generate_text(...) -> str
|
|
23
24
|
├── GeminiProvider.generate_json(...) -> dict | BaseModel
|
|
24
25
|
├── GeminiProvider.generate_image_bytes(...) -> tuple[bytes, str]
|
|
26
|
+
├── GeminiProvider.generate_imagen_bytes(...) -> tuple[bytes, str]
|
|
25
27
|
└── OpenAIProvider.generate_text(...) -> str
|
|
26
28
|
```
|
|
27
29
|
|
|
@@ -44,5 +46,7 @@ LLMRouter builder -> PromptResult -> LLMDispatcher.process() -> provider.answer(
|
|
|
44
46
|
|
|
45
47
|
- Возможности Gemini представлены напрямую, без широкой универсальной абстракции.
|
|
46
48
|
- JSON generation использует native JSON config провайдера и локальную проверку через `json.loads` и optional Pydantic schema.
|
|
47
|
-
-
|
|
49
|
+
- Gemini image generation и Imagen generation разведены в отдельные явные методы, потому что они используют разные SDK calls.
|
|
50
|
+
- `generate_image_bytes()` использует Gemini `generate_content` с image modality. Его `response_mime_type` является только preferred/fallback MIME type.
|
|
51
|
+
- `generate_imagen_bytes()` использует Imagen `generate_images` и передает requested MIME как `output_mime_type`.
|
|
48
52
|
- Anthropic, OpenRouter и multi-provider failover не являются активными API в этой alpha-линейке.
|
|
@@ -33,6 +33,18 @@ prompt: str
|
|
|
33
33
|
-> (bytes, actual_mime_type)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
`response_mime_type` в этом пути не передается в `GenerateContentConfig.response_mime_type`; он используется только как fallback content type, если Gemini не вернул `inline_data.mime_type`.
|
|
37
|
+
|
|
38
|
+
## Imagen Images
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
prompt: str
|
|
42
|
+
-> GeminiProvider.generate_imagen_bytes()
|
|
43
|
+
-> GenerateImagesConfig(output_mime_type=requested_mime)
|
|
44
|
+
-> first generated_images image
|
|
45
|
+
-> (bytes, actual_mime_type)
|
|
46
|
+
```
|
|
47
|
+
|
|
36
48
|
## OpenAI Text
|
|
37
49
|
|
|
38
50
|
```
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from codex_ai.core import (
|
|
4
4
|
ImageGenerationProvider,
|
|
5
|
+
ImagenGenerationProvider,
|
|
5
6
|
JsonGenerationProvider,
|
|
6
7
|
LLMDispatcher,
|
|
7
8
|
LLMMessage,
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
# Core
|
|
19
20
|
"LLMDispatcher",
|
|
20
21
|
"ImageGenerationProvider",
|
|
22
|
+
"ImagenGenerationProvider",
|
|
21
23
|
"JsonGenerationProvider",
|
|
22
24
|
"LLMMessage",
|
|
23
25
|
"LLMProviderError",
|
|
@@ -8,6 +8,7 @@ from .dispatcher import LLMDispatcher
|
|
|
8
8
|
from .exceptions import LLMProviderError
|
|
9
9
|
from .protocol import (
|
|
10
10
|
ImageGenerationProvider,
|
|
11
|
+
ImagenGenerationProvider,
|
|
11
12
|
JsonGenerationProvider,
|
|
12
13
|
LLMMessage,
|
|
13
14
|
LLMProviderProtocol,
|
|
@@ -22,6 +23,7 @@ __all__ = [
|
|
|
22
23
|
"LLMDispatcher",
|
|
23
24
|
"LLMProviderError",
|
|
24
25
|
"ImageGenerationProvider",
|
|
26
|
+
"ImagenGenerationProvider",
|
|
25
27
|
"JsonGenerationProvider",
|
|
26
28
|
"LLMMessage",
|
|
27
29
|
"LLMProviderProtocol",
|
|
@@ -14,6 +14,7 @@ from typing import Any
|
|
|
14
14
|
|
|
15
15
|
from .protocol import (
|
|
16
16
|
ImageGenerationProvider,
|
|
17
|
+
ImagenGenerationProvider,
|
|
17
18
|
JsonGenerationProvider,
|
|
18
19
|
LLMProviderProtocol,
|
|
19
20
|
PromptResult,
|
|
@@ -116,6 +117,33 @@ class LLMDispatcher:
|
|
|
116
117
|
**kwargs,
|
|
117
118
|
)
|
|
118
119
|
|
|
120
|
+
async def generate_imagen_bytes(
|
|
121
|
+
self,
|
|
122
|
+
prompt: str,
|
|
123
|
+
*,
|
|
124
|
+
model: str | None = None,
|
|
125
|
+
response_mime_type: str = "image/jpeg",
|
|
126
|
+
**kwargs: Any,
|
|
127
|
+
) -> tuple[bytes, str]:
|
|
128
|
+
"""
|
|
129
|
+
Generate Imagen bytes through a provider that supports Imagen generation.
|
|
130
|
+
|
|
131
|
+
This method is intentionally separate from ``generate_image_bytes()``
|
|
132
|
+
because Imagen uses ``generate_images`` and an image output MIME config.
|
|
133
|
+
"""
|
|
134
|
+
if not isinstance(self._provider, ImagenGenerationProvider):
|
|
135
|
+
provider_name = type(self._provider).__name__
|
|
136
|
+
raise TypeError(
|
|
137
|
+
f"Provider {provider_name} does not support Imagen generation; expected generate_imagen_bytes(...)"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return await self._provider.generate_imagen_bytes(
|
|
141
|
+
prompt,
|
|
142
|
+
model=model,
|
|
143
|
+
response_mime_type=response_mime_type,
|
|
144
|
+
**kwargs,
|
|
145
|
+
)
|
|
146
|
+
|
|
119
147
|
async def generate_text(
|
|
120
148
|
self,
|
|
121
149
|
prompt: PromptResult | str,
|
|
@@ -8,6 +8,7 @@ LLMProviderProtocol — adapter contract for LLM backends (OpenAI, Gemini, etc.)
|
|
|
8
8
|
TextGenerationProvider — optional adapter contract for direct text generation.
|
|
9
9
|
JsonGenerationProvider — optional adapter contract for direct JSON generation.
|
|
10
10
|
ImageGenerationProvider — optional adapter contract for binary image generation.
|
|
11
|
+
ImagenGenerationProvider — optional adapter contract for Imagen image generation.
|
|
11
12
|
PromptBuilder — type alias for async builder functions registered via LLMRouter.
|
|
12
13
|
"""
|
|
13
14
|
|
|
@@ -164,5 +165,37 @@ class ImageGenerationProvider(Protocol):
|
|
|
164
165
|
...
|
|
165
166
|
|
|
166
167
|
|
|
168
|
+
@runtime_checkable
|
|
169
|
+
class ImagenGenerationProvider(Protocol):
|
|
170
|
+
"""
|
|
171
|
+
Optional adapter contract for providers that expose Imagen image generation.
|
|
172
|
+
|
|
173
|
+
This is separate from ``ImageGenerationProvider`` because Gemini image models
|
|
174
|
+
and Imagen models use different SDK methods and MIME configuration fields.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
async def generate_imagen_bytes(
|
|
178
|
+
self,
|
|
179
|
+
prompt: str,
|
|
180
|
+
*,
|
|
181
|
+
model: str | None = None,
|
|
182
|
+
response_mime_type: str = "image/jpeg",
|
|
183
|
+
**kwargs: Any,
|
|
184
|
+
) -> tuple[bytes, str]:
|
|
185
|
+
"""
|
|
186
|
+
Generate an Imagen image and return raw bytes plus the actual content type.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
prompt: Plain image-generation prompt.
|
|
190
|
+
model: Optional Imagen model override.
|
|
191
|
+
response_mime_type: Requested image MIME type passed to Imagen when supported.
|
|
192
|
+
**kwargs: Extra provider-specific kwargs.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Tuple of ``(image_bytes, content_type)``.
|
|
196
|
+
"""
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
|
|
167
200
|
# Callable registered via @LLMRouter.prompt(mode)
|
|
168
201
|
PromptBuilder = Callable[..., Awaitable[PromptResult]]
|
|
@@ -8,6 +8,7 @@ Requires: ``pip install codex-ai[gemini]``
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import base64
|
|
11
12
|
import json
|
|
12
13
|
from typing import Any, cast
|
|
13
14
|
|
|
@@ -27,7 +28,8 @@ from codex_ai.core.exceptions import LLMProviderError
|
|
|
27
28
|
from codex_ai.core.protocol import PromptResult
|
|
28
29
|
|
|
29
30
|
_DEFAULT_MODEL = "gemini-2.5-flash-lite"
|
|
30
|
-
_DEFAULT_IMAGE_MODEL = "gemini-
|
|
31
|
+
_DEFAULT_IMAGE_MODEL = "gemini-2.5-flash-image"
|
|
32
|
+
_DEFAULT_IMAGEN_MODEL = "imagen-3.0-generate-002"
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class GeminiProvider:
|
|
@@ -39,7 +41,7 @@ class GeminiProvider:
|
|
|
39
41
|
Args:
|
|
40
42
|
api_key: Google AI API key.
|
|
41
43
|
model: Gemini text model name. Defaults to ``"gemini-2.5-flash-lite"``.
|
|
42
|
-
image_model: Gemini image model name. Defaults to ``"gemini-
|
|
44
|
+
image_model: Gemini image model name. Defaults to ``"gemini-2.5-flash-image"``.
|
|
43
45
|
|
|
44
46
|
Example:
|
|
45
47
|
```python
|
|
@@ -52,13 +54,20 @@ class GeminiProvider:
|
|
|
52
54
|
```
|
|
53
55
|
"""
|
|
54
56
|
|
|
55
|
-
def __init__(
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
api_key: str,
|
|
60
|
+
model: str = _DEFAULT_MODEL,
|
|
61
|
+
image_model: str = _DEFAULT_IMAGE_MODEL,
|
|
62
|
+
imagen_model: str = _DEFAULT_IMAGEN_MODEL,
|
|
63
|
+
) -> None:
|
|
56
64
|
self._client = genai.Client(
|
|
57
65
|
api_key=api_key,
|
|
58
66
|
http_options=genai_types.HttpOptions(api_version="v1alpha"),
|
|
59
67
|
)
|
|
60
68
|
self._model = model
|
|
61
69
|
self._image_model = image_model
|
|
70
|
+
self._imagen_model = imagen_model
|
|
62
71
|
|
|
63
72
|
async def answer(self, prompt: PromptResult, **kw: Any) -> str:
|
|
64
73
|
"""
|
|
@@ -180,10 +189,16 @@ class GeminiProvider:
|
|
|
180
189
|
**kwargs: Any,
|
|
181
190
|
) -> tuple[bytes, str]:
|
|
182
191
|
"""
|
|
183
|
-
Generate an image with Gemini and return
|
|
192
|
+
Generate an image with Gemini image models and return bytes plus content type.
|
|
193
|
+
|
|
194
|
+
This uses the Gemini ``generate_content`` image path with
|
|
195
|
+
``GenerateContentConfig.response_modalities=[IMAGE]``. Use it for Gemini
|
|
196
|
+
image preview / flash-image / nano-banana style models.
|
|
184
197
|
|
|
185
|
-
``response_mime_type`` is
|
|
186
|
-
|
|
198
|
+
``response_mime_type`` is a preferred/fallback MIME type only. Gemini's
|
|
199
|
+
``GenerateContentConfig.response_mime_type`` accepts text response MIME
|
|
200
|
+
values, so image MIME values are not sent there. The actual MIME type
|
|
201
|
+
returned in ``inline_data.mime_type`` wins when present.
|
|
187
202
|
"""
|
|
188
203
|
selected_model = model or self._image_model
|
|
189
204
|
requested_mime = response_mime_type
|
|
@@ -194,7 +209,6 @@ class GeminiProvider:
|
|
|
194
209
|
|
|
195
210
|
config = genai_types.GenerateContentConfig(
|
|
196
211
|
response_modalities=[genai_types.Modality.IMAGE],
|
|
197
|
-
response_mime_type=requested_mime,
|
|
198
212
|
**runtime_kw,
|
|
199
213
|
)
|
|
200
214
|
|
|
@@ -215,6 +229,52 @@ class GeminiProvider:
|
|
|
215
229
|
except Exception as exc:
|
|
216
230
|
raise LLMProviderError(f"Gemini image generation error: {exc}") from exc
|
|
217
231
|
|
|
232
|
+
async def generate_imagen_bytes(
|
|
233
|
+
self,
|
|
234
|
+
prompt: str,
|
|
235
|
+
*,
|
|
236
|
+
model: str | None = None,
|
|
237
|
+
response_mime_type: str = "image/jpeg",
|
|
238
|
+
**kwargs: Any,
|
|
239
|
+
) -> tuple[bytes, str]:
|
|
240
|
+
"""
|
|
241
|
+
Generate an image with Imagen models and return bytes plus content type.
|
|
242
|
+
|
|
243
|
+
This uses the Imagen ``generate_images`` SDK path and passes
|
|
244
|
+
``response_mime_type`` as ``GenerateImagesConfig.output_mime_type``.
|
|
245
|
+
Use it for ``imagen-*`` models, not Gemini flash-image / nano-banana
|
|
246
|
+
models.
|
|
247
|
+
"""
|
|
248
|
+
selected_model = model or self._imagen_model
|
|
249
|
+
requested_mime = response_mime_type
|
|
250
|
+
|
|
251
|
+
runtime_kw = kwargs.copy()
|
|
252
|
+
runtime_kw.pop("model", None)
|
|
253
|
+
runtime_kw.pop("response_mime_type", None)
|
|
254
|
+
runtime_kw.pop("output_mime_type", None)
|
|
255
|
+
|
|
256
|
+
config = genai_types.GenerateImagesConfig(
|
|
257
|
+
output_mime_type=requested_mime,
|
|
258
|
+
**runtime_kw,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
response = await self._client.aio.models.generate_images(
|
|
263
|
+
model=selected_model,
|
|
264
|
+
prompt=prompt,
|
|
265
|
+
config=config,
|
|
266
|
+
)
|
|
267
|
+
image = self._extract_first_imagen_image(response, fallback_mime=requested_mime)
|
|
268
|
+
if image is not None:
|
|
269
|
+
return image
|
|
270
|
+
|
|
271
|
+
detail = self._describe_imagen_non_image_response(response)
|
|
272
|
+
raise LLMProviderError(f"Gemini Imagen generation did not return image data{detail}")
|
|
273
|
+
except LLMProviderError:
|
|
274
|
+
raise
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
raise LLMProviderError(f"Gemini Imagen generation error: {exc}") from exc
|
|
277
|
+
|
|
218
278
|
@staticmethod
|
|
219
279
|
def _extract_first_inline_image(response: Any, *, fallback_mime: str) -> tuple[bytes, str] | None:
|
|
220
280
|
for part in GeminiProvider._iter_response_parts(response):
|
|
@@ -228,6 +288,45 @@ class GeminiProvider:
|
|
|
228
288
|
|
|
229
289
|
return None
|
|
230
290
|
|
|
291
|
+
@staticmethod
|
|
292
|
+
def _extract_first_imagen_image(response: Any, *, fallback_mime: str) -> tuple[bytes, str] | None:
|
|
293
|
+
for generated_image in getattr(response, "generated_images", None) or []:
|
|
294
|
+
image = getattr(generated_image, "image", None)
|
|
295
|
+
if image is None:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
data = getattr(image, "image_bytes", None)
|
|
299
|
+
if data is None:
|
|
300
|
+
data = getattr(image, "data", None)
|
|
301
|
+
if data is None:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
image_bytes = base64.b64decode(data) if isinstance(data, str) else bytes(data)
|
|
305
|
+
|
|
306
|
+
mime_type = getattr(image, "mime_type", None) or getattr(generated_image, "mime_type", None)
|
|
307
|
+
mime_type = mime_type or fallback_mime
|
|
308
|
+
return image_bytes, mime_type
|
|
309
|
+
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _describe_imagen_non_image_response(response: Any) -> str:
|
|
314
|
+
details: list[str] = []
|
|
315
|
+
|
|
316
|
+
for generated_image in getattr(response, "generated_images", None) or []:
|
|
317
|
+
rai_reason = getattr(generated_image, "rai_filtered_reason", None)
|
|
318
|
+
if rai_reason:
|
|
319
|
+
details.append(f"rai_filtered_reason={rai_reason}")
|
|
320
|
+
|
|
321
|
+
safety_attributes = getattr(generated_image, "safety_attributes", None)
|
|
322
|
+
if safety_attributes:
|
|
323
|
+
details.append(f"safety_attributes={safety_attributes}")
|
|
324
|
+
|
|
325
|
+
if not details:
|
|
326
|
+
return ""
|
|
327
|
+
|
|
328
|
+
return f": {'; '.join(details)}"
|
|
329
|
+
|
|
231
330
|
@staticmethod
|
|
232
331
|
def _describe_non_image_response(response: Any) -> str:
|
|
233
332
|
details: list[str] = []
|
|
@@ -131,6 +131,17 @@ class ImageProvider:
|
|
|
131
131
|
self.calls.append((prompt, model, response_mime_type, kwargs))
|
|
132
132
|
return b"image-bytes", "image/png"
|
|
133
133
|
|
|
134
|
+
async def generate_imagen_bytes(
|
|
135
|
+
self,
|
|
136
|
+
prompt: str,
|
|
137
|
+
*,
|
|
138
|
+
model: str | None = None,
|
|
139
|
+
response_mime_type: str = "image/jpeg",
|
|
140
|
+
**kwargs,
|
|
141
|
+
) -> tuple[bytes, str]:
|
|
142
|
+
self.calls.append((prompt, model, response_mime_type, kwargs))
|
|
143
|
+
return b"imagen-bytes", "image/jpeg"
|
|
144
|
+
|
|
134
145
|
async def generate_text(self, prompt: PromptResult | str, *, model: str | None = None, **kwargs) -> str:
|
|
135
146
|
self.calls.append((prompt, model, kwargs))
|
|
136
147
|
return "direct text"
|
|
@@ -167,6 +178,33 @@ async def test_dispatcher_generate_image_bytes_raises_for_unsupported_provider(m
|
|
|
167
178
|
assert mock_provider.calls == []
|
|
168
179
|
|
|
169
180
|
|
|
181
|
+
async def test_dispatcher_generate_imagen_bytes_delegates_to_imagen_provider():
|
|
182
|
+
provider = ImageProvider()
|
|
183
|
+
dispatcher = LLMDispatcher(provider=provider)
|
|
184
|
+
|
|
185
|
+
result = await dispatcher.generate_imagen_bytes(
|
|
186
|
+
"draw a castle",
|
|
187
|
+
model="imagen-model",
|
|
188
|
+
response_mime_type="image/jpeg",
|
|
189
|
+
seed=123,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert result == (b"imagen-bytes", "image/jpeg")
|
|
193
|
+
assert provider.calls == [("draw a castle", "imagen-model", "image/jpeg", {"seed": 123})]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def test_dispatcher_generate_imagen_bytes_raises_for_unsupported_provider(mock_provider):
|
|
197
|
+
dispatcher = LLMDispatcher(provider=mock_provider)
|
|
198
|
+
|
|
199
|
+
with pytest.raises(
|
|
200
|
+
TypeError,
|
|
201
|
+
match=r"Provider MockProvider does not support Imagen generation; expected generate_imagen_bytes\(\.\.\.\)",
|
|
202
|
+
):
|
|
203
|
+
await dispatcher.generate_imagen_bytes("draw a castle")
|
|
204
|
+
|
|
205
|
+
assert mock_provider.calls == []
|
|
206
|
+
|
|
207
|
+
|
|
170
208
|
async def test_dispatcher_generate_text_delegates_to_text_provider():
|
|
171
209
|
provider = ImageProvider()
|
|
172
210
|
dispatcher = LLMDispatcher(provider=provider)
|
|
@@ -3,6 +3,7 @@ from pydantic import ValidationError
|
|
|
3
3
|
|
|
4
4
|
from codex_ai.core.protocol import (
|
|
5
5
|
ImageGenerationProvider,
|
|
6
|
+
ImagenGenerationProvider,
|
|
6
7
|
JsonGenerationProvider,
|
|
7
8
|
LLMMessage,
|
|
8
9
|
LLMProviderProtocol,
|
|
@@ -116,6 +117,25 @@ def test_object_without_generate_image_bytes_fails_image_provider_check():
|
|
|
116
117
|
assert not isinstance(object(), ImageGenerationProvider)
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
def test_imagen_generation_provider_structural_check():
|
|
121
|
+
class MockImagenProvider:
|
|
122
|
+
async def generate_imagen_bytes(
|
|
123
|
+
self,
|
|
124
|
+
prompt: str,
|
|
125
|
+
*,
|
|
126
|
+
model: str | None = None,
|
|
127
|
+
response_mime_type: str = "image/jpeg",
|
|
128
|
+
**kwargs,
|
|
129
|
+
) -> tuple[bytes, str]:
|
|
130
|
+
return b"image", response_mime_type
|
|
131
|
+
|
|
132
|
+
assert isinstance(MockImagenProvider(), ImagenGenerationProvider)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_object_without_generate_imagen_bytes_fails_imagen_provider_check():
|
|
136
|
+
assert not isinstance(object(), ImagenGenerationProvider)
|
|
137
|
+
|
|
138
|
+
|
|
119
139
|
def test_text_generation_provider_structural_check():
|
|
120
140
|
class MockTextProvider:
|
|
121
141
|
async def generate_text(self, prompt: PromptResult | str, *, model: str | None = None, **kwargs) -> str:
|
|
@@ -9,6 +9,7 @@ from pydantic import BaseModel
|
|
|
9
9
|
from codex_ai.core.exceptions import LLMProviderError
|
|
10
10
|
from codex_ai.core.protocol import (
|
|
11
11
|
ImageGenerationProvider,
|
|
12
|
+
ImagenGenerationProvider,
|
|
12
13
|
JsonGenerationProvider,
|
|
13
14
|
LLMMessage,
|
|
14
15
|
LLMProviderProtocol,
|
|
@@ -49,6 +50,12 @@ def test_gemini_provider_satisfies_image_generation_protocol():
|
|
|
49
50
|
assert isinstance(provider, ImageGenerationProvider)
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
def test_gemini_provider_satisfies_imagen_generation_protocol():
|
|
54
|
+
with patch("codex_ai.providers.gemini.genai_types"):
|
|
55
|
+
provider = GeminiProvider(api_key="x")
|
|
56
|
+
assert isinstance(provider, ImagenGenerationProvider)
|
|
57
|
+
|
|
58
|
+
|
|
52
59
|
def test_gemini_provider_satisfies_text_generation_protocol():
|
|
53
60
|
with patch("codex_ai.providers.gemini.genai_types"):
|
|
54
61
|
provider = GeminiProvider(api_key="x")
|
|
@@ -355,6 +362,12 @@ def _image_response(data: bytes | bytearray = b"image", mime_type: str | None =
|
|
|
355
362
|
return SimpleNamespace(parts=[part], text=None)
|
|
356
363
|
|
|
357
364
|
|
|
365
|
+
def _imagen_response(data: bytes | str = b"image", mime_type: str | None = "image/jpeg") -> SimpleNamespace:
|
|
366
|
+
image = SimpleNamespace(image_bytes=data, mime_type=mime_type)
|
|
367
|
+
generated_image = SimpleNamespace(image=image)
|
|
368
|
+
return SimpleNamespace(generated_images=[generated_image])
|
|
369
|
+
|
|
370
|
+
|
|
358
371
|
async def test_gemini_generate_image_bytes_uses_image_model_not_text_model():
|
|
359
372
|
provider, mock_generate, _ = _make_provider()
|
|
360
373
|
provider._model = "text-model"
|
|
@@ -371,6 +384,19 @@ async def test_gemini_generate_image_bytes_uses_image_model_not_text_model():
|
|
|
371
384
|
assert kwargs["contents"] == "draw a castle"
|
|
372
385
|
|
|
373
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
|
+
|
|
374
400
|
async def test_gemini_generate_image_bytes_model_override_wins():
|
|
375
401
|
provider, mock_generate, _ = _make_provider()
|
|
376
402
|
provider._image_model = "image-model"
|
|
@@ -385,7 +411,7 @@ async def test_gemini_generate_image_bytes_model_override_wins():
|
|
|
385
411
|
assert kwargs["model"] == "explicit-image-model"
|
|
386
412
|
|
|
387
413
|
|
|
388
|
-
async def
|
|
414
|
+
async def test_gemini_generate_image_bytes_config_requests_image_modality_not_text_mime():
|
|
389
415
|
provider, mock_generate, _ = _make_provider()
|
|
390
416
|
mock_generate.return_value = _image_response()
|
|
391
417
|
|
|
@@ -396,7 +422,7 @@ async def test_gemini_generate_image_bytes_config_requests_image_modality_and_mi
|
|
|
396
422
|
|
|
397
423
|
config_kwargs = mock_types.GenerateContentConfig.call_args.kwargs
|
|
398
424
|
assert config_kwargs["response_modalities"] == ["IMAGE"]
|
|
399
|
-
assert
|
|
425
|
+
assert "response_mime_type" not in config_kwargs
|
|
400
426
|
assert config_kwargs["seed"] == 7
|
|
401
427
|
|
|
402
428
|
|
|
@@ -474,3 +500,111 @@ async def test_gemini_generate_image_bytes_wraps_sdk_errors():
|
|
|
474
500
|
await provider.generate_image_bytes("draw a castle")
|
|
475
501
|
|
|
476
502
|
assert exc_info.value.__cause__ is original
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
async def test_gemini_generate_imagen_bytes_uses_imagen_model_not_gemini_image_model():
|
|
506
|
+
provider, _, _ = _make_provider()
|
|
507
|
+
provider._image_model = "gemini-image-model"
|
|
508
|
+
provider._imagen_model = "imagen-model"
|
|
509
|
+
mock_generate_images = AsyncMock(return_value=_imagen_response())
|
|
510
|
+
provider._client.aio.models.generate_images = mock_generate_images
|
|
511
|
+
|
|
512
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
513
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
514
|
+
await provider.generate_imagen_bytes("draw a castle")
|
|
515
|
+
|
|
516
|
+
_, kwargs = mock_generate_images.call_args
|
|
517
|
+
assert kwargs["model"] == "imagen-model"
|
|
518
|
+
assert kwargs["prompt"] == "draw a castle"
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
async def test_gemini_generate_imagen_bytes_model_override_wins():
|
|
522
|
+
provider, _, _ = _make_provider()
|
|
523
|
+
provider._imagen_model = "imagen-model"
|
|
524
|
+
mock_generate_images = AsyncMock(return_value=_imagen_response())
|
|
525
|
+
provider._client.aio.models.generate_images = mock_generate_images
|
|
526
|
+
|
|
527
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
528
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
529
|
+
await provider.generate_imagen_bytes("draw a castle", model="explicit-imagen-model")
|
|
530
|
+
|
|
531
|
+
_, kwargs = mock_generate_images.call_args
|
|
532
|
+
assert kwargs["model"] == "explicit-imagen-model"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
async def test_gemini_generate_imagen_bytes_config_sets_output_mime_type():
|
|
536
|
+
provider, _, _ = _make_provider()
|
|
537
|
+
provider._client.aio.models.generate_images = AsyncMock(return_value=_imagen_response())
|
|
538
|
+
|
|
539
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
540
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
541
|
+
await provider.generate_imagen_bytes("draw a castle", response_mime_type="image/jpeg", seed=7)
|
|
542
|
+
|
|
543
|
+
config_kwargs = mock_types.GenerateImagesConfig.call_args.kwargs
|
|
544
|
+
assert config_kwargs["output_mime_type"] == "image/jpeg"
|
|
545
|
+
assert config_kwargs["seed"] == 7
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
async def test_gemini_generate_imagen_bytes_returns_image_bytes_and_actual_mime():
|
|
549
|
+
provider, _, _ = _make_provider()
|
|
550
|
+
provider._client.aio.models.generate_images = AsyncMock(
|
|
551
|
+
return_value=_imagen_response(data=b"jpeg-bytes", mime_type="image/jpeg")
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
555
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
556
|
+
result = await provider.generate_imagen_bytes("draw a castle", response_mime_type="image/png")
|
|
557
|
+
|
|
558
|
+
assert result == (b"jpeg-bytes", "image/jpeg")
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
async def test_gemini_generate_imagen_bytes_decodes_base64_image_bytes():
|
|
562
|
+
provider, _, _ = _make_provider()
|
|
563
|
+
provider._client.aio.models.generate_images = AsyncMock(
|
|
564
|
+
return_value=_imagen_response(data="anBlZy1ieXRlcw==", mime_type="image/jpeg")
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
568
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
569
|
+
result = await provider.generate_imagen_bytes("draw a castle")
|
|
570
|
+
|
|
571
|
+
assert result == (b"jpeg-bytes", "image/jpeg")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
async def test_gemini_generate_imagen_bytes_falls_back_to_requested_mime_when_missing():
|
|
575
|
+
provider, _, _ = _make_provider()
|
|
576
|
+
provider._client.aio.models.generate_images = AsyncMock(
|
|
577
|
+
return_value=_imagen_response(data=b"image-bytes", mime_type=None)
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
581
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
582
|
+
result = await provider.generate_imagen_bytes("draw a castle", response_mime_type="image/png")
|
|
583
|
+
|
|
584
|
+
assert result == (b"image-bytes", "image/png")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
async def test_gemini_generate_imagen_bytes_raises_when_no_image_data():
|
|
588
|
+
provider, _, _ = _make_provider()
|
|
589
|
+
generated_image = SimpleNamespace(image=SimpleNamespace(image_bytes=None, mime_type=None))
|
|
590
|
+
provider._client.aio.models.generate_images = AsyncMock(
|
|
591
|
+
return_value=SimpleNamespace(generated_images=[generated_image])
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
595
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
596
|
+
with pytest.raises(LLMProviderError, match="Imagen generation did not return image data"):
|
|
597
|
+
await provider.generate_imagen_bytes("draw a castle")
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
async def test_gemini_generate_imagen_bytes_wraps_sdk_errors():
|
|
601
|
+
provider, _, _ = _make_provider()
|
|
602
|
+
original = RuntimeError("quota exceeded")
|
|
603
|
+
provider._client.aio.models.generate_images = AsyncMock(side_effect=original)
|
|
604
|
+
|
|
605
|
+
with patch("codex_ai.providers.gemini.genai_types") as mock_types:
|
|
606
|
+
mock_types.GenerateImagesConfig.return_value = MagicMock()
|
|
607
|
+
with pytest.raises(LLMProviderError, match="Gemini Imagen generation error") as exc_info:
|
|
608
|
+
await provider.generate_imagen_bytes("draw a castle")
|
|
609
|
+
|
|
610
|
+
assert exc_info.value.__cause__ is original
|
|
@@ -20,6 +20,7 @@ def test_top_level_core_exports():
|
|
|
20
20
|
|
|
21
21
|
assert hasattr(codex_ai, "LLMDispatcher")
|
|
22
22
|
assert hasattr(codex_ai, "ImageGenerationProvider")
|
|
23
|
+
assert hasattr(codex_ai, "ImagenGenerationProvider")
|
|
23
24
|
assert hasattr(codex_ai, "JsonGenerationProvider")
|
|
24
25
|
assert hasattr(codex_ai, "LLMRouter")
|
|
25
26
|
assert hasattr(codex_ai, "LLMMessage")
|
|
@@ -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 = "==1.68.0" },
|
|
348
|
+
{ name = "google-genai", marker = "extra == 'dev'", specifier = "==1.68.0" },
|
|
349
|
+
{ name = "google-genai", marker = "extra == 'gemini'", specifier = "==1.68.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 = "1.68.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/9c/2c/f059982dbcb658cc535c81bbcbe7e2c040d675f4b563b03cdb01018a4bc3/google_genai-1.68.0.tar.gz", hash = "sha256:ac30c0b8bc630f9372993a97e4a11dae0e36f2e10d7c55eacdca95a9fa14ca96", size = 511285, upload-time = "2026-03-18T01:03:18.243Z" }
|
|
640
640
|
wheels = [
|
|
641
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
641
|
+
{ url = "https://files.pythonhosted.org/packages/84/de/7d3ee9c94b74c3578ea4f88d45e8de9405902f857932334d81e89bce3dfa/google_genai-1.68.0-py3-none-any.whl", hash = "sha256:a1bc9919c0e2ea2907d1e319b65471d3d6d58c54822039a249fe1323e4178d15", size = 750912, upload-time = "2026-03-18T01:03:15.983Z" },
|
|
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
|