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.
Files changed (66) hide show
  1. {codex_ai-0.2.0 → codex_ai-0.2.2}/CHANGELOG.md +15 -0
  2. {codex_ai-0.2.0 → codex_ai-0.2.2}/PKG-INFO +17 -5
  3. {codex_ai-0.2.0 → codex_ai-0.2.2}/README.md +13 -1
  4. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/providers/README.md +5 -1
  5. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/providers/data_flow.md +12 -0
  6. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/providers/README.md +5 -1
  7. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/providers/data_flow.md +12 -0
  8. {codex_ai-0.2.0 → codex_ai-0.2.2}/pyproject.toml +1 -1
  9. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/__init__.py +2 -0
  10. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/__init__.py +2 -0
  11. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/dispatcher.py +28 -0
  12. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/protocol.py +33 -0
  13. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/providers/gemini.py +106 -7
  14. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_dispatcher.py +38 -0
  15. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_protocol.py +20 -0
  16. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/providers/test_gemini_provider.py +136 -2
  17. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/test_public_api.py +1 -0
  18. {codex_ai-0.2.0 → codex_ai-0.2.2}/uv.lock +6 -6
  19. {codex_ai-0.2.0 → codex_ai-0.2.2}/.github/workflows/ci.yml +0 -0
  20. {codex_ai-0.2.0 → codex_ai-0.2.2}/.github/workflows/docs.yml +0 -0
  21. {codex_ai-0.2.0 → codex_ai-0.2.2}/.github/workflows/publish.yml +0 -0
  22. {codex_ai-0.2.0 → codex_ai-0.2.2}/.gitignore +0 -0
  23. {codex_ai-0.2.0 → codex_ai-0.2.2}/.nojekyll +0 -0
  24. {codex_ai-0.2.0 → codex_ai-0.2.2}/.pre-commit-config.yaml +0 -0
  25. {codex_ai-0.2.0 → codex_ai-0.2.2}/.python-version +0 -0
  26. {codex_ai-0.2.0 → codex_ai-0.2.2}/.secrets.baseline +0 -0
  27. {codex_ai-0.2.0 → codex_ai-0.2.2}/LICENSE +0 -0
  28. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/changelog.md +0 -0
  29. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/dispatcher.md +0 -0
  30. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/exceptions.md +0 -0
  31. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/protocol.md +0 -0
  32. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/router.md +0 -0
  33. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/core/sync.md +0 -0
  34. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/index.md +0 -0
  35. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/providers/gemini.md +0 -0
  36. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/api/providers/openai.md +0 -0
  37. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/core/README.md +0 -0
  38. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/en/architecture/core/data_flow.md +0 -0
  39. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/index.md +0 -0
  40. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/core/README.md +0 -0
  41. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/ru/architecture/core/data_flow.md +0 -0
  42. {codex_ai-0.2.0 → codex_ai-0.2.2}/docs/stylesheets/extra.css +0 -0
  43. {codex_ai-0.2.0 → codex_ai-0.2.2}/mkdocs.yml +0 -0
  44. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/exceptions.py +0 -0
  45. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/router.py +0 -0
  46. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/core/sync.py +0 -0
  47. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/providers/__init__.py +0 -0
  48. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/providers/openai.py +0 -0
  49. {codex_ai-0.2.0 → codex_ai-0.2.2}/src/codex_ai/py.typed +0 -0
  50. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/conftest.py +0 -0
  51. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/integration/__init__.py +0 -0
  52. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/integration/conftest.py +0 -0
  53. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/integration/test_providers_integration.py +0 -0
  54. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/__init__.py +0 -0
  55. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/conftest.py +0 -0
  56. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/__init__.py +0 -0
  57. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_exceptions.py +0 -0
  58. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_router.py +0 -0
  59. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/core/test_sync.py +0 -0
  60. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/providers/__init__.py +0 -0
  61. {codex_ai-0.2.0 → codex_ai-0.2.2}/tests/unit/providers/test_openai_provider.py +0 -0
  62. {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/__init__.py +0 -0
  63. {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/dev/README.md +0 -0
  64. {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/dev/__init__.py +0 -0
  65. {codex_ai-0.2.0 → codex_ai-0.2.2}/tools/dev/check.py +0 -0
  66. {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.0
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>=1.0; extra == 'all'
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>=1.0; extra == 'dev'
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>=1.0; extra == 'gemini'
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, and image generation via `google-genai` |
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, and image generation via `google-genai` |
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
- - Image generation returns raw bytes plus the actual MIME type returned by Gemini.
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
- - Image generation возвращает raw bytes и фактический MIME type от Gemini.
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
  ```
@@ -31,7 +31,7 @@ Issues = "https://github.com/codexdlc/codex-ai/issues"
31
31
 
32
32
  [project.optional-dependencies]
33
33
  openai = ["openai>=1.0,<2.0"]
34
- gemini = ["google-genai>=1.0"]
34
+ gemini = ["google-genai==1.68.0"]
35
35
  all = [
36
36
  "codex-ai[openai,gemini]",
37
37
  ]
@@ -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-3.1-flash-image-preview"
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-3.1-flash-image-preview"``.
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__(self, api_key: str, model: str = _DEFAULT_MODEL, image_model: str = _DEFAULT_IMAGE_MODEL) -> None:
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 raw bytes plus content type.
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 treated as the requested MIME type. The actual
186
- MIME type returned by Gemini wins when present.
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 test_gemini_generate_image_bytes_config_requests_image_modality_and_mime():
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 config_kwargs["response_mime_type"] == "image/webp"
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 = ">=1.0" },
348
- { name = "google-genai", marker = "extra == 'dev'", specifier = ">=1.0" },
349
- { name = "google-genai", marker = "extra == 'gemini'", specifier = ">=1.0" },
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 = "2.3.0"
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/02/8e/dfa4b34dd4c0baffccf6466fc68d6d35011662d43e7d79accb902320db74/google_genai-2.3.0.tar.gz", hash = "sha256:e877c750a4ccacdd9928fc3aa8ca8820ce85cade0ca51bd83feceacf5959b579", size = 546930, upload-time = "2026-05-15T06:22:36.264Z" }
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/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" },
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