codex-ai 0.2.3__tar.gz → 0.2.5__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 (68) hide show
  1. {codex_ai-0.2.3 → codex_ai-0.2.5}/CHANGELOG.md +18 -0
  2. {codex_ai-0.2.3 → codex_ai-0.2.5}/PKG-INFO +55 -13
  3. {codex_ai-0.2.3 → codex_ai-0.2.5}/README.md +50 -8
  4. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/architecture/core/README.md +19 -15
  5. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/architecture/core/data_flow.md +5 -3
  6. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/architecture/providers/README.md +19 -5
  7. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/architecture/providers/data_flow.md +35 -2
  8. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/ru/architecture/core/README.md +18 -15
  9. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/ru/architecture/core/data_flow.md +5 -3
  10. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/ru/architecture/providers/README.md +19 -5
  11. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/ru/architecture/providers/data_flow.md +35 -2
  12. {codex_ai-0.2.3 → codex_ai-0.2.5}/pyproject.toml +2 -2
  13. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/__init__.py +4 -0
  14. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/core/__init__.py +5 -1
  15. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/core/dispatcher.py +9 -5
  16. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/core/protocol.py +32 -10
  17. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/providers/__init__.py +1 -1
  18. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/providers/gemini.py +45 -8
  19. codex_ai-0.2.5/src/codex_ai/providers/openai.py +265 -0
  20. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/integration/test_providers_integration.py +3 -3
  21. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/core/test_dispatcher.py +12 -3
  22. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/core/test_protocol.py +9 -0
  23. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/providers/test_gemini_provider.py +29 -0
  24. codex_ai-0.2.5/tests/unit/providers/test_openai_provider.py +272 -0
  25. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/test_public_api.py +1 -0
  26. {codex_ai-0.2.3 → codex_ai-0.2.5}/uv.lock +103 -98
  27. codex_ai-0.2.3/src/codex_ai/providers/openai.py +0 -137
  28. codex_ai-0.2.3/tests/unit/providers/test_openai_provider.py +0 -283
  29. {codex_ai-0.2.3 → codex_ai-0.2.5}/.github/workflows/ci.yml +0 -0
  30. {codex_ai-0.2.3 → codex_ai-0.2.5}/.github/workflows/docs.yml +0 -0
  31. {codex_ai-0.2.3 → codex_ai-0.2.5}/.github/workflows/publish.yml +0 -0
  32. {codex_ai-0.2.3 → codex_ai-0.2.5}/.gitignore +0 -0
  33. {codex_ai-0.2.3 → codex_ai-0.2.5}/.nojekyll +0 -0
  34. {codex_ai-0.2.3 → codex_ai-0.2.5}/.pre-commit-config.yaml +0 -0
  35. {codex_ai-0.2.3 → codex_ai-0.2.5}/.python-version +0 -0
  36. {codex_ai-0.2.3 → codex_ai-0.2.5}/.secrets.baseline +0 -0
  37. {codex_ai-0.2.3 → codex_ai-0.2.5}/LICENSE +0 -0
  38. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/changelog.md +0 -0
  39. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/core/dispatcher.md +0 -0
  40. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/core/exceptions.md +0 -0
  41. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/core/protocol.md +0 -0
  42. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/core/router.md +0 -0
  43. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/core/sync.md +0 -0
  44. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/index.md +0 -0
  45. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/providers/gemini.md +0 -0
  46. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/en/api/providers/openai.md +0 -0
  47. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/index.md +0 -0
  48. {codex_ai-0.2.3 → codex_ai-0.2.5}/docs/stylesheets/extra.css +0 -0
  49. {codex_ai-0.2.3 → codex_ai-0.2.5}/mkdocs.yml +0 -0
  50. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/core/exceptions.py +0 -0
  51. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/core/router.py +0 -0
  52. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/core/sync.py +0 -0
  53. {codex_ai-0.2.3 → codex_ai-0.2.5}/src/codex_ai/py.typed +0 -0
  54. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/conftest.py +0 -0
  55. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/integration/__init__.py +0 -0
  56. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/integration/conftest.py +0 -0
  57. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/__init__.py +0 -0
  58. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/conftest.py +0 -0
  59. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/core/__init__.py +0 -0
  60. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/core/test_exceptions.py +0 -0
  61. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/core/test_router.py +0 -0
  62. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/core/test_sync.py +0 -0
  63. {codex_ai-0.2.3 → codex_ai-0.2.5}/tests/unit/providers/__init__.py +0 -0
  64. {codex_ai-0.2.3 → codex_ai-0.2.5}/tools/__init__.py +0 -0
  65. {codex_ai-0.2.3 → codex_ai-0.2.5}/tools/dev/README.md +0 -0
  66. {codex_ai-0.2.3 → codex_ai-0.2.5}/tools/dev/__init__.py +0 -0
  67. {codex_ai-0.2.3 → codex_ai-0.2.5}/tools/dev/check.py +0 -0
  68. {codex_ai-0.2.3 → codex_ai-0.2.5}/tools/dev/generate_project_tree.py +0 -0
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.5] - 2026-07-19
8
+
9
+ ### Added
10
+ - Added `ImageInput` and optional `input_images` support to Gemini `generate_image_bytes()` for image reference/edit workflows.
11
+ - Added OpenAI structured output generation through `generate_json()` with native Pydantic parsing.
12
+ - Added typed OpenAI Responses API text streaming through `stream_text()`.
13
+
14
+ ### Changed
15
+ - Replaced the OpenAI Chat Completions implementation with the Responses API.
16
+ - Updated the OpenAI SDK requirement to `openai>=2.0,<3.0`.
17
+ - Changed the OpenAI default model to `gpt-5.6-luna` with `reasoning={"effort": "none"}` and `store=False` defaults.
18
+
19
+ ## [0.2.4] - 2026-05-21
20
+
21
+ ### Changed
22
+ - Widened the supported `codex-core` range to include the `0.4.x` series.
23
+ - Clarified the Gemini-first provider focus in the package documentation.
24
+
7
25
  ## [0.2.3] - 2026-05-17
8
26
 
9
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-ai
3
- Version: 0.2.3
3
+ Version: 0.2.5
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/
@@ -16,17 +16,17 @@ Classifier: License :: OSI Approved :: Apache Software License
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: >=3.12
19
- Requires-Dist: codex-core<0.4.0,>=0.2.2
19
+ Requires-Dist: codex-core<0.5.0,>=0.2.2
20
20
  Requires-Dist: pydantic<3.0,>=2.0
21
21
  Provides-Extra: all
22
22
  Requires-Dist: google-genai==2.3.0; extra == 'all'
23
- Requires-Dist: openai<2.0,>=1.0; extra == 'all'
23
+ Requires-Dist: openai<3.0,>=2.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
27
  Requires-Dist: google-genai==2.3.0; extra == 'dev'
28
28
  Requires-Dist: mypy>=1.10; extra == 'dev'
29
- Requires-Dist: openai<2.0,>=1.0; extra == 'dev'
29
+ Requires-Dist: openai<3.0,>=2.0; extra == 'dev'
30
30
  Requires-Dist: pip-audit>=2.7; extra == 'dev'
31
31
  Requires-Dist: pre-commit>=3.0; extra == 'dev'
32
32
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
@@ -42,17 +42,17 @@ Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
42
42
  Provides-Extra: gemini
43
43
  Requires-Dist: google-genai==2.3.0; extra == 'gemini'
44
44
  Provides-Extra: openai
45
- Requires-Dist: openai<2.0,>=1.0; extra == 'openai'
45
+ Requires-Dist: openai<3.0,>=2.0; extra == 'openai'
46
46
  Description-Content-Type: text/markdown
47
47
 
48
48
  # codex-ai <!-- Type: LANDING -->
49
49
 
50
50
  [![PyPI version](https://img.shields.io/pypi/v/codex-ai.svg)](https://pypi.org/project/codex-ai/)
51
51
  [![Python](https://img.shields.io/pypi/pyversions/codex-ai.svg)](https://pypi.org/project/codex-ai/)
52
- [![CI](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml)
52
+ [![CI](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml)
53
53
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/codexdlc/codex-ai/blob/main/LICENSE)
54
54
 
55
- Gemini-first and OpenAI provider helpers for the Codex ecosystem. The library keeps the legacy prompt router for text generation, and exposes direct provider methods for practical Gemini workflows.
55
+ Gemini-first API helpers for the Codex ecosystem. The active surface is direct Gemini text, JSON, Gemini image, and Imagen generation. OpenAI provides modern Responses API text, structured JSON, and streaming helpers. The router/dispatcher layer is kept for legacy text workflows.
56
56
 
57
57
  ## Install
58
58
 
@@ -68,9 +68,11 @@ Requires Python 3.12 or newer.
68
68
  ## Gemini Direct API
69
69
 
70
70
  ```python
71
+ from pathlib import Path
72
+
71
73
  from pydantic import BaseModel
72
74
 
73
- from codex_ai import GeminiProvider
75
+ from codex_ai import GeminiProvider, ImageInput
74
76
 
75
77
 
76
78
  class LootItem(BaseModel):
@@ -89,6 +91,13 @@ image_bytes, content_type = await gemini.generate_image_bytes(
89
91
  image_config={"aspect_ratio": "1:1", "image_size": "4K"},
90
92
  )
91
93
 
94
+ source_image_bytes = Path("source.png").read_bytes()
95
+ edited_bytes, edited_content_type = await gemini.generate_image_bytes(
96
+ "Keep the composition, but redraw it as a watercolor map.",
97
+ response_mime_type="image/png",
98
+ input_images=[ImageInput(data=source_image_bytes, mime_type="image/png")],
99
+ )
100
+
92
101
  imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
93
102
  "A fantasy clan banner, game icon style.",
94
103
  response_mime_type="image/jpeg",
@@ -101,11 +110,40 @@ imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
101
110
  `response_mime_type` as a preferred/fallback MIME type. It does not pass image MIME values
102
111
  to Gemini's text `response_mime_type` config field. Pass Gemini image controls such as
103
112
  `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
113
+ the Gemini provider retries once with `2K`. Pass `input_images` with `ImageInput`
114
+ items to provide reference/edit-source images for Gemini image models. Use `generate_imagen_bytes()` for
105
115
  Imagen models; that path uses `generate_images` and passes the requested MIME as
106
116
  `output_mime_type`.
107
117
 
108
- ## Router Pipeline
118
+ ## OpenAI Responses API
119
+
120
+ ```python
121
+ from pydantic import BaseModel
122
+
123
+ from codex_ai import OpenAIProvider
124
+
125
+
126
+ class LootItem(BaseModel):
127
+ name: str
128
+ power: int
129
+
130
+
131
+ openai = OpenAIProvider(api_key="sk-...")
132
+
133
+ text = await openai.generate_text("Write one short tavern rumor.")
134
+ loot = await openai.generate_json("Create one loot item.", schema=LootItem)
135
+
136
+ async for chunk in openai.stream_text("Tell a short story."):
137
+ print(chunk, end="")
138
+ ```
139
+
140
+ The OpenAI provider uses the Responses API and the `openai` 2.x SDK. Responses
141
+ are not stored by default. Its default `gpt-5.6-luna` model uses
142
+ `reasoning={"effort": "none"}` to retain a cost- and latency-sensitive role;
143
+ override the model and reasoning options per request when a workload needs more
144
+ capability.
145
+
146
+ ## Legacy Text Router
109
147
 
110
148
  ```python
111
149
  from codex_ai import GeminiProvider, LLMDispatcher, LLMMessage, LLMRouter, PromptResult
@@ -127,13 +165,17 @@ dispatcher.include_router(router)
127
165
  response = await dispatcher.process("chat", text="Hello!")
128
166
  ```
129
167
 
168
+ Use this path only when you already have prompt builders registered through `LLMRouter`.
169
+ New Gemini integrations should call `generate_text()`, `generate_json()`,
170
+ `generate_image_bytes()`, or `generate_imagen_bytes()` directly.
171
+
130
172
  ## Modules
131
173
 
132
174
  | Module | Extra | Description |
133
175
  | :--- | :--- | :--- |
134
- | `codex_ai.core` | - | Dispatcher, router, protocol types, sync wrapper, and shared exception contract |
135
- | `codex_ai.providers.gemini` | `[gemini]` | Google Gemini text, JSON, Gemini image, and Imagen generation via pinned `google-genai` |
136
- | `codex_ai.providers.openai` | `[openai]` | OpenAI Chat Completions text provider |
176
+ | `codex_ai.providers.gemini` | `[gemini]` | Primary API: Gemini text, JSON, Gemini image, and Imagen generation via pinned `google-genai` |
177
+ | `codex_ai.providers.openai` | `[openai]` | OpenAI Responses API text, structured JSON, and streaming adapter |
178
+ | `codex_ai.core` | - | Legacy text router/dispatcher contracts and shared provider exceptions |
137
179
 
138
180
  ## Development
139
181
 
@@ -2,10 +2,10 @@
2
2
 
3
3
  [![PyPI version](https://img.shields.io/pypi/v/codex-ai.svg)](https://pypi.org/project/codex-ai/)
4
4
  [![Python](https://img.shields.io/pypi/pyversions/codex-ai.svg)](https://pypi.org/project/codex-ai/)
5
- [![CI](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml)
5
+ [![CI](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/codexdlc/codex-ai/actions/workflows/ci.yml)
6
6
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/codexdlc/codex-ai/blob/main/LICENSE)
7
7
 
8
- Gemini-first and OpenAI provider helpers for the Codex ecosystem. The library keeps the legacy prompt router for text generation, and exposes direct provider methods for practical Gemini workflows.
8
+ Gemini-first API helpers for the Codex ecosystem. The active surface is direct Gemini text, JSON, Gemini image, and Imagen generation. OpenAI provides modern Responses API text, structured JSON, and streaming helpers. The router/dispatcher layer is kept for legacy text workflows.
9
9
 
10
10
  ## Install
11
11
 
@@ -21,9 +21,11 @@ Requires Python 3.12 or newer.
21
21
  ## Gemini Direct API
22
22
 
23
23
  ```python
24
+ from pathlib import Path
25
+
24
26
  from pydantic import BaseModel
25
27
 
26
- from codex_ai import GeminiProvider
28
+ from codex_ai import GeminiProvider, ImageInput
27
29
 
28
30
 
29
31
  class LootItem(BaseModel):
@@ -42,6 +44,13 @@ image_bytes, content_type = await gemini.generate_image_bytes(
42
44
  image_config={"aspect_ratio": "1:1", "image_size": "4K"},
43
45
  )
44
46
 
47
+ source_image_bytes = Path("source.png").read_bytes()
48
+ edited_bytes, edited_content_type = await gemini.generate_image_bytes(
49
+ "Keep the composition, but redraw it as a watercolor map.",
50
+ response_mime_type="image/png",
51
+ input_images=[ImageInput(data=source_image_bytes, mime_type="image/png")],
52
+ )
53
+
45
54
  imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
46
55
  "A fantasy clan banner, game icon style.",
47
56
  response_mime_type="image/jpeg",
@@ -54,11 +63,40 @@ imagen_bytes, imagen_content_type = await gemini.generate_imagen_bytes(
54
63
  `response_mime_type` as a preferred/fallback MIME type. It does not pass image MIME values
55
64
  to Gemini's text `response_mime_type` config field. Pass Gemini image controls such as
56
65
  `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
66
+ the Gemini provider retries once with `2K`. Pass `input_images` with `ImageInput`
67
+ items to provide reference/edit-source images for Gemini image models. Use `generate_imagen_bytes()` for
58
68
  Imagen models; that path uses `generate_images` and passes the requested MIME as
59
69
  `output_mime_type`.
60
70
 
61
- ## Router Pipeline
71
+ ## OpenAI Responses API
72
+
73
+ ```python
74
+ from pydantic import BaseModel
75
+
76
+ from codex_ai import OpenAIProvider
77
+
78
+
79
+ class LootItem(BaseModel):
80
+ name: str
81
+ power: int
82
+
83
+
84
+ openai = OpenAIProvider(api_key="sk-...")
85
+
86
+ text = await openai.generate_text("Write one short tavern rumor.")
87
+ loot = await openai.generate_json("Create one loot item.", schema=LootItem)
88
+
89
+ async for chunk in openai.stream_text("Tell a short story."):
90
+ print(chunk, end="")
91
+ ```
92
+
93
+ The OpenAI provider uses the Responses API and the `openai` 2.x SDK. Responses
94
+ are not stored by default. Its default `gpt-5.6-luna` model uses
95
+ `reasoning={"effort": "none"}` to retain a cost- and latency-sensitive role;
96
+ override the model and reasoning options per request when a workload needs more
97
+ capability.
98
+
99
+ ## Legacy Text Router
62
100
 
63
101
  ```python
64
102
  from codex_ai import GeminiProvider, LLMDispatcher, LLMMessage, LLMRouter, PromptResult
@@ -80,13 +118,17 @@ dispatcher.include_router(router)
80
118
  response = await dispatcher.process("chat", text="Hello!")
81
119
  ```
82
120
 
121
+ Use this path only when you already have prompt builders registered through `LLMRouter`.
122
+ New Gemini integrations should call `generate_text()`, `generate_json()`,
123
+ `generate_image_bytes()`, or `generate_imagen_bytes()` directly.
124
+
83
125
  ## Modules
84
126
 
85
127
  | Module | Extra | Description |
86
128
  | :--- | :--- | :--- |
87
- | `codex_ai.core` | - | Dispatcher, router, protocol types, sync wrapper, and shared exception contract |
88
- | `codex_ai.providers.gemini` | `[gemini]` | Google Gemini text, JSON, Gemini image, and Imagen generation via pinned `google-genai` |
89
- | `codex_ai.providers.openai` | `[openai]` | OpenAI Chat Completions text provider |
129
+ | `codex_ai.providers.gemini` | `[gemini]` | Primary API: Gemini text, JSON, Gemini image, and Imagen generation via pinned `google-genai` |
130
+ | `codex_ai.providers.openai` | `[openai]` | OpenAI Responses API text, structured JSON, and streaming adapter |
131
+ | `codex_ai.core` | - | Legacy text router/dispatcher contracts and shared provider exceptions |
90
132
 
91
133
  ## Development
92
134
 
@@ -2,19 +2,22 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- `codex_ai.core` is the orchestration layer for LLM interactions. It decouples prompt construction from provider selection — prompt logic is defined once and can be routed to any backend without changes.
5
+ `codex_ai.core` is the legacy text orchestration layer. It keeps existing `LLMRouter`/`LLMDispatcher` prompt-builder workflows working while the active API surface moves to direct provider methods.
6
6
 
7
7
  ## Why It's a Module
8
8
 
9
- Working directly with LLM SDKs across a codebase creates three recurring problems:
9
+ Older Codex integrations use mode-based prompt builders. This module keeps that shape stable without making it the primary abstraction for new work:
10
10
 
11
- | Problem | What breaks |
12
- |---------|-------------|
13
- | Prompt logic scattered across call sites | Hard to test, review, or reuse |
14
- | Provider SDK calls coupled to business code | Switching providers requires touching every call |
15
- | No unified contract for async/sync contexts | Different wiring for Django views, ARQ workers, bots |
11
+ | Need | Current role |
12
+ |------|--------------|
13
+ | Keep registered prompt builders working | `LLMRouter` maps modes to builders |
14
+ | Run existing text flows without rewriting callers | `LLMDispatcher.process()` still calls `provider.answer()` |
15
+ | Bridge sync-only contexts | `SyncLLMDispatcher` remains available for CLI/WSGI code |
16
16
 
17
- `core` solves these by introducing a clean pipeline:
17
+ For new Gemini work, prefer `GeminiProvider.generate_text()`, `generate_json()`,
18
+ `generate_image_bytes()`, and `generate_imagen_bytes()` directly.
19
+
20
+ The retained text pipeline is:
18
21
 
19
22
  ```
20
23
  @router.prompt("mode") → PromptResult (frozen DTO) → LLMProviderProtocol.answer()
@@ -32,7 +35,7 @@ Working directly with LLM SDKs across a codebase creates three recurring problem
32
35
  │ include_router(router)
33
36
  ▼
34
37
  ┌──────────────────────┐
35
- │ LLMDispatcher │ orchestrates builder → provider
38
+ │ LLMDispatcher │ legacy text builder → provider
36
39
  │ │
37
40
  │ .process(mode, **kw)│
38
41
  └──────┬───────────────┘
@@ -52,19 +55,20 @@ Working directly with LLM SDKs across a codebase creates three recurring problem
52
55
 
53
56
  | Component | Class | Role |
54
57
  |-----------|-------|------|
55
- | `protocol.py` | `PromptResult` | Frozen DTO — immutable prompt passed to providers |
56
- | `protocol.py` | `LLMProviderProtocol` | Structural protocol — any class with `async answer()` qualifies |
58
+ | `protocol.py` | `PromptResult` | Frozen DTO used by legacy text prompt builders |
59
+ | `protocol.py` | `LLMProviderProtocol` | Structural text compatibility protocol for `answer()` |
57
60
  | `protocol.py` | `PromptBuilder` | Type alias for `async def (...) -> PromptResult` |
58
61
  | `router.py` | `LLMRouter` | Registry: maps `mode` strings to builder functions via decorator |
59
- | `dispatcher.py` | `LLMDispatcher` | Wires router + provider; single entry point for prompt execution |
62
+ | `dispatcher.py` | `LLMDispatcher` | Runs legacy text prompts and delegates direct provider convenience methods |
60
63
  | `sync.py` | `SyncLLMDispatcher` | Wraps `LLMDispatcher` with `asyncio.run()` for WSGI/CLI contexts |
61
64
  | `exceptions.py` | `LLMProviderError` | Base exception raised by all provider implementations |
62
65
 
63
66
  ## Key Design Decisions
64
67
 
65
- - **Frozen DTO (`PromptResult`)** — built once by the builder, cannot be mutated downstream. Prevents accidental state sharing between requests.
66
- - **`@runtime_checkable` Protocol** — `isinstance(obj, LLMProviderProtocol)` works at runtime. No inheritance required from any base class.
67
- - **Mode-based dispatch** — `dispatcher.process("chat", ...)` maps to a registered builder. Adding a new prompt type never touches existing code.
68
+ - **Direct provider APIs first** — Gemini capabilities are exposed as explicit methods instead of being forced through a universal provider interface.
69
+ - **Frozen DTO (`PromptResult`)** — kept for legacy builders and cannot be mutated downstream.
70
+ - **`@runtime_checkable` Protocol** — retained for runtime checks at compatibility boundaries.
71
+ - **Mode-based dispatch is legacy text infrastructure** — `dispatcher.process("chat", ...)` maps to a registered builder for existing flows.
68
72
  - **All logs at `DEBUG`** — dispatcher emits only debug-level messages. Production log level controls visibility without code changes.
69
73
  - **`SyncLLMDispatcher` for Django only** — uses `asyncio.run()` which creates a new event loop. Never call from inside an async context (ARQ, async views, bots) — use `LLMDispatcher` directly.
70
74
 
@@ -1,6 +1,8 @@
1
1
  # Core — Data Flow
2
2
 
3
- ## Request Lifecycle
3
+ ## Legacy Text Request Lifecycle
4
+
5
+ This flow is retained for existing `LLMRouter` integrations. New Gemini work should use direct provider methods.
4
6
 
5
7
  ```
6
8
  1. Application calls:
@@ -13,14 +15,14 @@
13
15
  prompt = await builder(text="Hello!")
14
16
  # → PromptResult(messages=[LLMMessage(role="user", content="Hello!")])
15
17
 
16
- 4. Dispatcher calls provider:
18
+ 4. Dispatcher calls the text compatibility method:
17
19
  response = await self._provider.answer(prompt, **kw)
18
20
 
19
21
  5. Provider returns text:
20
22
  "Hi there! How can I help?"
21
23
  ```
22
24
 
23
- ## Component Interactions
25
+ ## Legacy Component Interactions
24
26
 
25
27
  ```
26
28
  Application
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- `codex_ai.providers` contains concrete provider adapters for the APIs currently supported by the library: Gemini and OpenAI.
5
+ `codex_ai.providers` contains concrete SDK adapters, not a broad interchangeable provider framework. Gemini is the product focus; OpenAI is a separate modern Responses API adapter.
6
6
 
7
7
  Gemini is the primary target and exposes direct methods for text, JSON, and image generation:
8
8
 
@@ -13,7 +13,14 @@ await gemini.generate_image_bytes(...)
13
13
  await gemini.generate_imagen_bytes(...)
14
14
  ```
15
15
 
16
- OpenAI is kept as a text provider with the same `generate_text(...)` convenience.
16
+ OpenAI exposes direct text, structured JSON, and typed streaming helpers:
17
+
18
+ ```python
19
+ await openai.generate_text(...)
20
+ await openai.generate_json(..., schema=MyModel)
21
+ async for chunk in openai.stream_text(...):
22
+ ...
23
+ ```
17
24
 
18
25
  ## Architecture
19
26
 
@@ -24,7 +31,9 @@ PromptResult/String
24
31
  ├── GeminiProvider.generate_json(...) -> dict | BaseModel
25
32
  ├── GeminiProvider.generate_image_bytes(...) -> tuple[bytes, str]
26
33
  ├── GeminiProvider.generate_imagen_bytes(...) -> tuple[bytes, str]
27
- └── OpenAIProvider.generate_text(...) -> str
34
+ ├── OpenAIProvider.generate_text(...) -> str
35
+ ├── OpenAIProvider.generate_json(...) -> dict | BaseModel
36
+ └── OpenAIProvider.stream_text(...) -> AsyncIterator[str]
28
37
  ```
29
38
 
30
39
  The legacy router pipeline remains available:
@@ -40,14 +49,19 @@ LLMRouter builder -> PromptResult -> LLMDispatcher.process() -> provider.answer(
40
49
  | Component | Class | SDK | Default Model |
41
50
  |-----------|-------|-----|---------------|
42
51
  | `gemini.py` | `GeminiProvider` | `google-genai` | `gemini-2.5-flash-lite` |
43
- | `openai.py` | `OpenAIProvider` | `openai` | `gpt-4o-mini` |
52
+ | `openai.py` | `OpenAIProvider` | `openai` 2.x | `gpt-5.6-luna` |
44
53
 
45
54
  ## Key Design Decisions
46
55
 
47
56
  - Gemini-specific capabilities are represented directly instead of being hidden behind a broad universal abstraction.
48
57
  - JSON generation uses provider-native JSON configuration and still validates locally with `json.loads` and optional Pydantic models.
58
+ - OpenAI uses the Responses API exclusively; the old Chat Completions path is not retained.
59
+ - OpenAI requests default to `store=False` and `reasoning={"effort": "none"}`.
60
+ - OpenAI Pydantic schemas use the SDK's native `responses.parse()` structured-output path.
61
+ - The OpenAI SDK client is injectable behind a small Responses API port for deterministic tests and custom transports.
49
62
  - Gemini image generation and Imagen generation are separate explicit methods because they use different SDK calls.
50
63
  - `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`.
64
+ - `generate_image_bytes()` can also accept `input_images=[ImageInput(...)]` for image reference/edit workflows; without `input_images`, the prompt-only path is unchanged.
51
65
  - `generate_image_bytes()` retries a rejected `image_config={"image_size": "4K"}` request once as `2K`.
52
66
  - `generate_imagen_bytes()` uses Imagen `generate_images` and passes the requested MIME as `output_mime_type`.
53
- - Anthropic, OpenRouter, and multi-provider failover are not active APIs in this alpha line.
67
+ - Anthropic, OpenRouter, and multi-provider failover are not active APIs in this line.
@@ -33,6 +33,16 @@ prompt: str
33
33
  -> (bytes, actual_mime_type)
34
34
  ```
35
35
 
36
+ With `input_images`, the provider sends multimodal content:
37
+
38
+ ```
39
+ prompt: str + input_images: list[ImageInput]
40
+ -> text part + inline_data image parts
41
+ -> GeminiProvider.generate_image_bytes()
42
+ -> first returned inline_data image part
43
+ -> (bytes, actual_mime_type)
44
+ ```
45
+
36
46
  `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
47
  When `image_config.image_size` is `4K` and Gemini rejects the request, the provider retries once with `image_size` changed to `2K`.
38
48
 
@@ -51,7 +61,30 @@ prompt: str
51
61
  ```
52
62
  PromptResult | str
53
63
  -> OpenAIProvider.generate_text()
54
- -> chat.completions.create()
55
- -> choices[0].message.content
64
+ -> _OpenAIRequestMapper
65
+ -> responses.create(store=False)
66
+ -> response.output_text
56
67
  -> str
57
68
  ```
69
+
70
+ ## OpenAI Structured JSON
71
+
72
+ ```
73
+ PromptResult | str + Pydantic schema
74
+ -> OpenAIProvider.generate_json()
75
+ -> responses.parse(text_format=schema)
76
+ -> response.output_parsed
77
+ -> BaseModel
78
+ ```
79
+
80
+ Without a schema, the provider requests JSON mode and decodes `response.output_text`.
81
+
82
+ ## OpenAI Streaming
83
+
84
+ ```
85
+ PromptResult | str
86
+ -> OpenAIProvider.stream_text()
87
+ -> responses.create(stream=True)
88
+ -> response.output_text.delta events
89
+ -> AsyncIterator[str]
90
+ ```
@@ -2,19 +2,21 @@
2
2
 
3
3
  ## Назначение
4
4
 
5
- `codex_ai.core` — оркестрационный слой для работы с LLM. Разделяет построение промпта и выбор провайдера: логика промптов описывается один раз и может быть направлена к любому бэкенду без изменений.
5
+ `codex_ai.core` — legacy-слой текстовой оркестрации. Он сохраняет существующие workflow на `LLMRouter`/`LLMDispatcher`, пока активная API-поверхность переехала в прямые методы провайдеров.
6
6
 
7
7
  ## Зачем это модуль
8
8
 
9
- Прямая работа с LLM SDK по всей кодовой базе создаёт три повторяющихся проблемы:
9
+ Старые Codex-интеграции используют mode-based prompt builders. Этот модуль сохраняет такую форму, но больше не является основной абстракцией для новой работы:
10
10
 
11
- | Проблема | Что ломается |
12
- |----------|-------------|
13
- | Логика промптов разбросана по call site-ам | Трудно тестировать, ревьювить и переиспользовать |
14
- | Вызовы SDK провайдера связаны с бизнес-кодом | Смена провайдера требует правок в каждом вызове |
15
- | Нет единого контракта для async/sync контекстов | Разная обвязка для Django views, ARQ workers, ботов |
11
+ | Потребность | Текущая роль |
12
+ |-------------|--------------|
13
+ | Сохранить зарегистрированные prompt builders | `LLMRouter` связывает modes с билдерами |
14
+ | Не переписывать существующие текстовые вызовы | `LLMDispatcher.process()` продолжает вызывать `provider.answer()` |
15
+ | Поддержать sync-only контексты | `SyncLLMDispatcher` остается для CLI/WSGI кода |
16
16
 
17
- `core` решает это через единый pipeline:
17
+ Для новой Gemini-интеграции лучше использовать прямые методы `GeminiProvider.generate_text()`, `generate_json()`, `generate_image_bytes()` и `generate_imagen_bytes()`.
18
+
19
+ Сохраненный текстовый pipeline:
18
20
 
19
21
  ```
20
22
  @router.prompt("mode") → PromptResult (frozen DTO) → LLMProviderProtocol.answer()
@@ -32,7 +34,7 @@
32
34
  │ include_router(router)
33
35
  ▼
34
36
  ┌──────────────────────┐
35
- │ LLMDispatcher │ оркестрирует builder → provider
37
+ │ LLMDispatcher │ legacy text builder → provider
36
38
  │ │
37
39
  │ .process(mode, **kw)│
38
40
  └──────┬───────────────┘
@@ -52,19 +54,20 @@
52
54
 
53
55
  | Компонент | Класс | Роль |
54
56
  |-----------|-------|------|
55
- | `protocol.py` | `PromptResult` | Frozen DTO — иммутабельный промпт, передаваемый провайдерам |
56
- | `protocol.py` | `LLMProviderProtocol` | Структурный протокол — любой класс с `async answer()` подходит |
57
+ | `protocol.py` | `PromptResult` | Frozen DTO для legacy text prompt builders |
58
+ | `protocol.py` | `LLMProviderProtocol` | Структурный текстовый compatibility-протокол для `answer()` |
57
59
  | `protocol.py` | `PromptBuilder` | Type alias для `async def (...) -> PromptResult` |
58
60
  | `router.py` | `LLMRouter` | Реестр: связывает строки `mode` с функциями-билдерами через декоратор |
59
- | `dispatcher.py` | `LLMDispatcher` | Связывает router + provider; единая точка входа для выполнения промптов |
61
+ | `dispatcher.py` | `LLMDispatcher` | Выполняет legacy text prompts и делегирует прямые provider convenience methods |
60
62
  | `sync.py` | `SyncLLMDispatcher` | Оборачивает `LLMDispatcher` через `asyncio.run()` для WSGI/CLI |
61
63
  | `exceptions.py` | `LLMProviderError` | Базовое исключение, поднимаемое всеми реализациями провайдеров |
62
64
 
63
65
  ## Ключевые решения
64
66
 
65
- - **Frozen DTO (`PromptResult`)** — строится один раз в билдере, не может быть изменён downstream. Исключает случайный shared state между запросами.
66
- - **`@runtime_checkable` Protocol** — `isinstance(obj, LLMProviderProtocol)` работает в runtime. Наследование от базового класса не требуется.
67
- - **Mode-based dispatch** — `dispatcher.process("chat", ...)` находит зарегистрированный билдер. Добавление нового типа промпта не затрагивает существующий код.
67
+ - **Direct provider APIs first** — возможности Gemini раскрываются явными методами, а не проталкиваются через универсальный provider interface.
68
+ - **Frozen DTO (`PromptResult`)** — сохранен для legacy builders и не может быть изменён downstream.
69
+ - **`@runtime_checkable` Protocol** — оставлен для runtime-проверок на compatibility boundaries.
70
+ - **Mode-based dispatch — legacy text infrastructure** — `dispatcher.process("chat", ...)` находит зарегистрированный билдер для существующих flows.
68
71
  - **Все логи на `DEBUG`** — диспетчер пишет только debug-сообщения. Уровень логирования в продакшне управляет видимостью без изменений кода.
69
72
  - **`SyncLLMDispatcher` только для Django** — использует `asyncio.run()`, создающий новый event loop. Никогда не вызывать из async-контекста (ARQ, async views, боты) — используйте `LLMDispatcher` напрямую.
70
73
 
@@ -1,6 +1,8 @@
1
1
  # Core — Поток данных
2
2
 
3
- ## Жизненный цикл запроса
3
+ ## Жизненный цикл legacy text запроса
4
+
5
+ Этот flow сохранен для существующих интеграций на `LLMRouter`. Новую Gemini-интеграцию лучше писать через прямые методы провайдера.
4
6
 
5
7
  ```
6
8
  1. Приложение вызывает:
@@ -13,14 +15,14 @@
13
15
  prompt = await builder(text="Привет!")
14
16
  # → PromptResult(messages=[LLMMessage(role="user", content="Привет!")])
15
17
 
16
- 4. Диспетчер вызывает провайдер:
18
+ 4. Диспетчер вызывает текстовый compatibility method:
17
19
  response = await self._provider.answer(prompt, **kw)
18
20
 
19
21
  5. Провайдер возвращает текст:
20
22
  "Привет! Чем могу помочь?"
21
23
  ```
22
24
 
23
- ## Взаимодействие компонентов
25
+ ## Взаимодействие legacy-компонентов
24
26
 
25
27
  ```
26
28
  Приложение
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Назначение
4
4
 
5
- `codex_ai.providers` содержит адаптеры для API, которые сейчас реально поддерживаются библиотекой: Gemini и OpenAI.
5
+ `codex_ai.providers` содержит конкретные SDK-адаптеры, а не широкий interchangeable provider framework. Фокус продукта — Gemini; OpenAI является отдельным современным адаптером Responses API.
6
6
 
7
7
  Gemini является основным направлением и дает прямые методы для текста, JSON и картинок:
8
8
 
@@ -13,7 +13,14 @@ await gemini.generate_image_bytes(...)
13
13
  await gemini.generate_imagen_bytes(...)
14
14
  ```
15
15
 
16
- OpenAI оставлен как текстовый провайдер с `generate_text(...)`.
16
+ OpenAI предоставляет прямые методы для текста, структурированного JSON и typed streaming:
17
+
18
+ ```python
19
+ await openai.generate_text(...)
20
+ await openai.generate_json(..., schema=MyModel)
21
+ async for chunk in openai.stream_text(...):
22
+ ...
23
+ ```
17
24
 
18
25
  ## Архитектура
19
26
 
@@ -24,7 +31,9 @@ PromptResult/String
24
31
  ├── GeminiProvider.generate_json(...) -> dict | BaseModel
25
32
  ├── GeminiProvider.generate_image_bytes(...) -> tuple[bytes, str]
26
33
  ├── GeminiProvider.generate_imagen_bytes(...) -> tuple[bytes, str]
27
- └── OpenAIProvider.generate_text(...) -> str
34
+ ├── OpenAIProvider.generate_text(...) -> str
35
+ ├── OpenAIProvider.generate_json(...) -> dict | BaseModel
36
+ └── OpenAIProvider.stream_text(...) -> AsyncIterator[str]
28
37
  ```
29
38
 
30
39
  Старый router pipeline остается доступным:
@@ -40,14 +49,19 @@ LLMRouter builder -> PromptResult -> LLMDispatcher.process() -> provider.answer(
40
49
  | Компонент | Класс | SDK | Модель по умолчанию |
41
50
  |-----------|-------|-----|---------------------|
42
51
  | `gemini.py` | `GeminiProvider` | `google-genai` | `gemini-2.5-flash-lite` |
43
- | `openai.py` | `OpenAIProvider` | `openai` | `gpt-4o-mini` |
52
+ | `openai.py` | `OpenAIProvider` | `openai` 2.x | `gpt-5.6-luna` |
44
53
 
45
54
  ## Ключевые решения
46
55
 
47
56
  - Возможности Gemini представлены напрямую, без широкой универсальной абстракции.
48
57
  - JSON generation использует native JSON config провайдера и локальную проверку через `json.loads` и optional Pydantic schema.
58
+ - OpenAI использует только Responses API; старый путь Chat Completions не сохраняется.
59
+ - OpenAI-запросы по умолчанию используют `store=False` и `reasoning={"effort": "none"}`.
60
+ - Pydantic-схемы OpenAI обрабатываются через native structured-output путь `responses.parse()`.
61
+ - OpenAI SDK client можно внедрить через небольшой Responses API port для детерминированных тестов и custom transport.
49
62
  - Gemini image generation и Imagen generation разведены в отдельные явные методы, потому что они используют разные SDK calls.
50
63
  - `generate_image_bytes()` использует Gemini `generate_content` с image modality. Его `response_mime_type` является только preferred/fallback MIME type, а Gemini image controls передаются через `image_config`.
64
+ - `generate_image_bytes()` также принимает `input_images=[ImageInput(...)]` для сценариев reference/edit; без `input_images` старый prompt-only путь не меняется.
51
65
  - `generate_image_bytes()` один раз повторяет отклоненный `image_config={"image_size": "4K"}` запрос как `2K`.
52
66
  - `generate_imagen_bytes()` использует Imagen `generate_images` и передает requested MIME как `output_mime_type`.
53
- - Anthropic, OpenRouter и multi-provider failover не являются активными API в этой alpha-линейке.
67
+ - Anthropic, OpenRouter и multi-provider failover не являются активными API в этой линейке.