python-codex 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

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 (53) hide show
  1. pycodex/__init__.py +3 -27
  2. pycodex/bootstrap.py +5 -45
  3. pycodex/cli.py +1 -14
  4. pycodex/context.py +1 -1
  5. pycodex/events.py +1 -1
  6. pycodex/model.py +2 -2
  7. pycodex/portable/__init__.py +45 -0
  8. pycodex/{portable.py → portable/client.py} +4 -7
  9. pycodex/{portable_server.py → portable/server.py} +3 -3
  10. pycodex/protocol.py +1 -1
  11. pycodex/runtime.py +1 -1
  12. pycodex/tools/base_tool.py +1 -1
  13. pycodex/tools/close_agent_tool.py +1 -1
  14. pycodex/tools/code_mode_manager.py +1 -1
  15. pycodex/tools/request_permissions_tool.py +1 -1
  16. pycodex/tools/resume_agent_tool.py +1 -1
  17. pycodex/{runtime_services.py → tools/runtime_services.py} +5 -5
  18. pycodex/tools/send_input_tool.py +1 -1
  19. pycodex/tools/spawn_agent_tool.py +1 -1
  20. pycodex/tools/unified_exec_manager.py +1 -1
  21. pycodex/tools/update_plan_tool.py +1 -1
  22. pycodex/tools/view_image_tool.py +5 -0
  23. pycodex/tools/wait_agent_tool.py +1 -1
  24. pycodex/utils/__init__.py +0 -4
  25. pycodex/utils/_aes_gcm/LICENSE +56 -0
  26. pycodex/utils/_aes_gcm/README.md +61 -0
  27. pycodex/utils/_aes_gcm/__init__.py +1 -0
  28. pycodex/utils/_aes_gcm/aes.py +175 -0
  29. pycodex/utils/_aes_gcm/gcm.py +73 -0
  30. pycodex/utils/_aes_gcm/source-sha256.json +4 -0
  31. pycodex/{compat.py → utils/compat.py} +2 -0
  32. pycodex/utils/crypto.py +35 -0
  33. pycodex/utils/debug.py +24 -0
  34. pycodex/{doctor.py → utils/doctor.py} +3 -3
  35. pycodex/utils/image_utils.py +3 -2
  36. pycodex/{model_metadata.py → utils/model_metadata.py} +2 -2
  37. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/METADATA +165 -42
  38. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/RECORD +43 -45
  39. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/entry_points.txt +0 -1
  40. responses_server/app.py +28 -15
  41. responses_server/stream_router.py +17 -13
  42. pycodex/feishu_card.py +0 -793
  43. pycodex/feishu_link.py +0 -440
  44. pycodex/tools/ipython_tool.py +0 -142
  45. workspace_server/__init__.py +0 -39
  46. workspace_server/__main__.py +0 -4
  47. workspace_server/app.py +0 -1713
  48. workspace_server/owned_process.py +0 -27
  49. workspace_server/workspace.html +0 -2211
  50. workspace_server/workspaces.html +0 -550
  51. workspace_server/workspaces.py +0 -628
  52. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/WHEEL +0 -0
  53. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,20 +1,17 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A minimal Python extraction of Codex's main agent loop
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.6.2
7
7
  Requires-Dist: dataclasses>=0.8; python_version < '3.7'
8
- Requires-Dist: fastapi<0.84,>=0.83.0; python_version < '3.7'
9
- Requires-Dist: fastapi>=0.83.0; python_version >= '3.7'
10
8
  Requires-Dist: importlib-metadata>=4.8.3; python_version < '3.8'
11
9
  Requires-Dist: loguru>=0.7.3
12
- Requires-Dist: pillow>=8.4.0; python_version < '3.7'
13
- Requires-Dist: pillow>=9.0.0; python_version >= '3.7'
14
10
  Requires-Dist: prompt-toolkit>=3.0.36
15
- Requires-Dist: pycryptodomex>=3.20
16
11
  Requires-Dist: pyyaml>=6.0
17
12
  Requires-Dist: requests>=2.27.1
13
+ Requires-Dist: starlette<0.20,>=0.19.1; python_version < '3.7'
14
+ Requires-Dist: starlette>=0.19.1; python_version >= '3.7'
18
15
  Requires-Dist: tomli<2,>=1.2.3; python_version < '3.11'
19
16
  Requires-Dist: typing-extensions>=4.1.1; python_version < '3.8'
20
17
  Requires-Dist: uvicorn<0.17,>=0.16.0; python_version < '3.7'
@@ -25,21 +22,43 @@ Description-Content-Type: text/markdown
25
22
 
26
23
  English README. Chinese version: `README_ZH.md`
27
24
 
25
+ 0.4.0 core/workspace package split and dependency cleanup: [release notes](docs/RELEASE_0.4.0.md).
26
+
27
+ 0.3.3 workspace viewers and reasoning stream fix: [release notes](docs/RELEASE_0.3.3.md).
28
+
28
29
  0.3.2 startup fixes: [release notes](docs/RELEASE_0.3.2.md).
29
30
 
30
31
  0.3.0 release preparation and Python API migration: `docs/RELEASE_0.3.0.md`.
31
32
 
32
33
  PyPI distributions:
33
34
 
34
- - Primary package: `python-codex`
35
- - Workspace install alias: `pycodex-ws`
35
+ - Core package: `python-codex`
36
+ - Workspace and integrations: `pycodex-ws`
36
37
 
37
38
  The import path remains `pycodex`; the CLI commands are `pycodex` and
38
39
  `pycodex-ws`.
39
40
 
40
- `pycodex-ws` is a thin metapackage that depends on the exact matching
41
- `python-codex` version. The implementation and console script remain owned by
42
- `python-codex`, so the two distributions never install duplicate modules.
41
+ `python-codex` contains the Agent, runtime, CLI, standard tools, Responses
42
+ compatibility server and portable storage support. `pycodex-ws` depends on the
43
+ exact matching core version and contains the browser workspace, viewer assets,
44
+ Feishu and IPython integrations, including their dependencies. It owns the
45
+ `workspace_server` and `pycodex_ws` modules and the `pycodex-ws` command.
46
+
47
+ Core does not require Pillow. Without it, `view_image` sends the original image
48
+ bytes with `detail="original"`, including when `high` is requested. With Pillow
49
+ installed, the default path retains resizing to a maximum dimension of 2048.
50
+ `pycodex-ws` installs Pillow for image preview validation.
51
+
52
+ Portable encryption uses a bundled pure Python AES-GCM subset of pyaes and
53
+ tlslite-ng, under MIT and BSD licenses. Core does not require a crypto package
54
+ or compiler and ships a `py3-none-any` wheel. Existing `PCX1` bundles and call
55
+ tokens remain compatible; large bundles take longer to encrypt and decrypt.
56
+ See the [source notes](pycodex/utils/_aes_gcm/README.md).
57
+
58
+ The Responses compatibility server and workspace HTTP server use Starlette
59
+ and Uvicorn directly. Neither package requires FastAPI, Pydantic or
60
+ `pydantic-core`. Python 3.6 uses Starlette 0.19.x; newer interpreters resolve
61
+ a compatible Starlette release through its Python version metadata.
43
62
 
44
63
  This repository extracts the core Codex agent loop from upstream Codex
45
64
  (`https://github.com/openai/codex`) into a deliberately small Python version,
@@ -59,16 +78,17 @@ Relevant Rust reference points:
59
78
 
60
79
  ## Quick Start
61
80
 
62
- Install the full package or the workspace-oriented alias:
81
+ Install the core CLI, or install the workspace package to include all integrations:
63
82
 
64
83
  ```bash
65
84
  python3 -m pip install python-codex
66
85
  python3 -m pip install pycodex-ws
67
86
  ```
68
87
 
69
- Use Python 3.6.2 or newer. Both console commands are installed into that
70
- interpreter's scripts directory. On a machine with multiple Python installations,
71
- you can select the same interpreter explicitly for startup:
88
+ The core supports Python 3.6.2 or newer. The workspace package requires Python
89
+ 3.7 or newer because of its Feishu SDK dependency. Commands are installed into
90
+ that interpreter's scripts directory. On a machine with multiple Python
91
+ installations, select the same interpreter explicitly for startup:
72
92
 
73
93
  ```bash
74
94
  python3 -m pycodex
@@ -79,10 +99,14 @@ If `pycodex` is missing from `PATH` or resolves to an older installation, use th
79
99
  module entry points. `python3 -m pip --version` shows which Python environment
80
100
  received the installation.
81
101
 
102
+ For an existing Web installation, use `python3 -m pip install --upgrade pycodex-ws`
103
+ to upgrade the workspace and its matching core together. Installing only
104
+ `python-codex` provides the CLI and core API.
105
+
82
106
  For development from this repository, install dependencies first:
83
107
 
84
108
  ```bash
85
- uv sync
109
+ uv sync --dev
86
110
  ```
87
111
 
88
112
  Try the real entry points:
@@ -92,8 +116,22 @@ uv run pycodex "Reply with exactly OK."
92
116
  uv run pycodex
93
117
  ```
94
118
 
95
- A VS Code frontend is available in [`extensions/vscode`](extensions/vscode/README.md).
96
- It reuses an existing `python-codex>=0.3.1` installation. On first use, missing
119
+ Tests are split by package. `uv run --dev pytest` runs both suites;
120
+ `uv run --dev pytest packages/pycodex-ws/tests` runs workspace and integration
121
+ tests. To check core without installing workspace or its SDKs, use a separate
122
+ environment:
123
+
124
+ ```bash
125
+ UV_PROJECT_ENVIRONMENT=.tmp/core-tests uv sync --group test
126
+ UV_PROJECT_ENVIRONMENT=.tmp/core-tests uv run --group test pytest tests
127
+ ```
128
+
129
+ See [test environments and coverage](tests/TESTS.md). Core tests support Python
130
+ 3.6.2+; workspace tests require Python 3.7+.
131
+
132
+ A VS Code frontend is available in [`packages/pycodex-ws/extensions/vscode`](packages/pycodex-ws/extensions/vscode/README.md).
133
+ It belongs to the workspace frontend and is packaged separately as a VSIX.
134
+ It reuses an existing `pycodex-ws>=0.4.0` installation. On first use, missing
97
135
  or older backends are installed/upgraded with pip in the selected Python
98
136
  environment on the workspace host; compatible versions need no network check.
99
137
  It starts a dedicated `pycodex-ws` workspace on the project's host and opens a
@@ -141,19 +179,35 @@ nailing the core tool-augmented reasoning loop first.
141
179
 
142
180
  ## Layout
143
181
 
182
+ The top level of `pycodex/` contains the core components and package entry points.
183
+ Supporting services live in `tools/`, `portable/` and `utils/`.
184
+
185
+ - `pycodex/agent.py`: inner turn loop
186
+ - `pycodex/runtime.py`: session commands, submission queues and frontend event subscriptions
187
+ - `pycodex/model.py`: model client protocol and Responses API adapter
188
+ - `pycodex/context.py`: configuration, prompt assembly and instruction loading
144
189
  - `pycodex/protocol.py`: minimal conversation item / prompt protocol
145
190
  - `pycodex/events.py`: typed events, plain-text views and stateful presentation
146
- - `pycodex/utils/event_helpers.py`: stateless text, color, summary and result-formatting helpers
147
- - `pycodex/utils/image_utils.py`: image loading, resizing and data-URL preparation
148
- - `pycodex/model.py`: model client protocol and Responses API adapter
149
- - `pycodex/cli.py`: single-turn/interactive entry points, terminal I/O executor and input loop
150
191
  - `pycodex/bootstrap.py`: frontend-independent model, tools, Agent and session assembly
192
+ - `pycodex/cli.py`: single-turn/interactive entry points, terminal I/O executor and input loop
151
193
  - `pycodex/tools/base_tool.py`: `BaseTool`, `ToolRegistry`, `ToolContext`
152
- - `pycodex/tools/`: concrete tool implementations
153
- - `pycodex/agent.py`: inner turn loop
154
- - `pycodex/runtime.py`: session commands, submission queues and frontend event subscriptions
155
- - `extensions/vscode/`: VS Code chat frontend using the workspace HTTP API
156
- - `tests/test_agent.py`: core behavior tests
194
+ - `pycodex/tools/runtime_services.py`: plan, sub-agent, user-input and permissions services
195
+ - `pycodex/tools/`: concrete tools and their execution managers
196
+ - `pycodex/portable/`: Codex home storage client and server, both included in core
197
+ - `pycodex/utils/compat.py`: supported-Python compatibility helpers
198
+ - `pycodex/utils/doctor.py`: configuration and connectivity diagnostics
199
+ - `pycodex/utils/model_metadata.py`: vendored model metadata lookup
200
+ - `pycodex/utils/debug.py`: debug paths and the `pycodex.debug()` implementation
201
+ - `pycodex/utils/event_helpers.py`: stateless text, color, summary and result-formatting helpers
202
+ - `pycodex/utils/image_utils.py`: image loading, resizing and data-URL preparation
203
+ - `pycodex/prompts/`: vendored prompts and model metadata
204
+ - `packages/pycodex-ws/workspace_server/`: Web board, API and file/diff viewers
205
+ - `packages/pycodex-ws/pycodex_ws/`: Feishu, IPython and workspace assembly
206
+ - `packages/pycodex-ws/extensions/vscode/`: workspace's VS Code frontend, built separately as a VSIX
207
+ - `tests/`: core tests
208
+ - `packages/pycodex-ws/tests/`: workspace and cross-frontend integration tests
209
+
210
+ See [package layout and import paths](docs/RUNTIME.md#package-layout).
157
211
 
158
212
  ## Current Alignment Status
159
213
 
@@ -264,6 +318,53 @@ Current behavior:
264
318
  - with an argv prompt or piped stdin, run a single turn
265
319
  - `pycodex-ws` starts the standalone browser workspace manager and serves each
266
320
  workspace with a board pane and a pycodex session pane
321
+ - the left pane is the board, containing an optional live HTML Canvas tab and closable read-only file/diff tabs that
322
+ scroll together in one toolbar; a mouse wheel scrolls overflowing tab strips
323
+ horizontally.
324
+ Click Markdown file links such as `[source](src/app.py:42)` or
325
+ `[source](/data/project/app.py#L42)` to open a view. Git-style diff links use
326
+ `[changes](diff:HEAD:src/app.py)` for working changes,
327
+ `[versions](diff:HEAD~1..HEAD:src/app.py)` for two revisions, and
328
+ `[files](diff:old.py..new.py)` for two local files. Append `#L42` or `#R42`
329
+ to a diff link to select a 1-based line on the left or right.
330
+ Views open when the user clicks; model replies alone do not create tabs.
331
+ Reopening the same file or comparison selects its existing tab in that
332
+ browser, including when the line changes. Other pages keep their selection.
333
+ Tabs show filenames first, adding a short directory suffix
334
+ for duplicate names. Git versions use short commit IDs in a secondary label.
335
+ Long labels use the same width limit as session tabs, shortening the secondary
336
+ information before the filename;
337
+ hover titles retain the complete paths and revisions. Text,
338
+ Markdown, HTML, common raster images and text diffs are supported. Monaco
339
+ provides syntax highlighting, find, folding and character-level diffs.
340
+ Right-click code and choose **Insert into chat**, or select text and use the
341
+ toolbar button, to insert context at the current chat draft's cursor.
342
+ Context appears as a separate block among your words. Click it to edit or
343
+ remove it; Backspace and Delete also remove a block as one element.
344
+ Context identifies the full view label, including both revisions of a diff.
345
+ Selections include only the selected text and range; otherwise it records the
346
+ cursor location. Diff context also identifies the selected side.
347
+ The draft stays editable until you send it.
348
+ Its code and styles load only when a code or diff view opens, so Canvas,
349
+ Markdown and HTML previews do not load the editor. Viewer assets use gzip
350
+ when supported and revalidate cached content with ETags.
351
+ Narrow content panes automatically use a unified diff; targeting the left
352
+ side keeps both sides visible. Markdown and HTML links default to previews;
353
+ line targets open their source. Clicking a link without a line returns that
354
+ same tab to preview. Manual tab switches preserve the chosen mode and reading state.
355
+ All views remain
356
+ read-only, with text selection and copying available. Local files update
357
+ automatically, including both sides of file diffs and the working side of
358
+ Git diffs; Git revisions stay fixed. Updates keep the active editor and
359
+ reading state without replaying an earlier line request.
360
+ Canvas is for explanations and custom visuals; source and diffs use links.
361
+ `[Canvas](workspace://canvas)` returns to the live page; an HTML file link opens
362
+ a preview tab, and `page.html:1` opens its source. File links work even with an empty `--toolset`.
363
+ Canvas and HTML previews share the same origin and script permissions;
364
+ the browser loads their resources with the workspace login cookie.
365
+ Open views survive reloads and server restarts. See
366
+ [workspace content views](docs/RUNTIME.md#workspace-content-views) for the
367
+ complete syntax and limits.
267
368
  - interactive mode exits through `/exit`, `/quit`, Ctrl+D on an empty prompt,
268
369
  or a single Ctrl+C; accepted work finishes before cleanup and normal exit.
269
370
  A `[closing]` message explains the wait. Press Ctrl+C again while closing
@@ -312,28 +413,37 @@ Current behavior:
312
413
  non-persisted follow-up session and submits the file contents as the next
313
414
  user instruction; this is intended for side-effect follow-ups such as
314
415
  Feishu notifications
315
- - `/link <feishu-email|open_id|chat_id>` attaches the current interactive
316
- session to a Feishu card; multiple sessions in the same pycodex process share
416
+ - `/link <feishu-email|open_id|chat_id>` in a workspace session attaches it
417
+ to a Feishu card; multiple sessions in the same workspace process share
317
418
  one Feishu long-connection listener and route card actions by message id
318
419
  - Feishu user OAuth stores its refresh token only in
319
420
  `~/.codex/.feishu_refresh_token` (mode `0600`). Run
320
- `env -u VIRTUAL_ENV uv run python tools/feishu_oauth.py` from this repository
321
- to authorize; app credentials can stay in `~/.codex/.env`. Each refresh reads
421
+ `python3 -m pycodex_ws.feishu_oauth` after installing `pycodex-ws` to authorize.
422
+ From this repository, use `env -u VIRTUAL_ENV uv run --dev python -m pycodex_ws.feishu_oauth`.
423
+ App credentials can stay in `~/.codex/.env`. Each refresh reads
322
424
  the latest token from the dedicated file and saves its replacement there.
323
425
  `FEISHU_REFRESH_TOKEN` in `.env` or the environment is no longer used and
324
426
  can be removed. Refresh is triggered by API calls, with no idle keepalive.
325
427
  - `pycodex-ws --workspace-config workspaces.json` serves workspace boards from
326
428
  one process. The JSON file can be either a list or
327
- `{"workspaces": [...]}`; each entry uses `board` and `work_dir`, with optional
328
- display/URL `id`, and is exposed at `/w/<id>/`. Internally the resolved
329
- `board` path is the workspace identity, so adding the same board path twice is
330
- rejected even with a different name. Relative `board` and `work_dir` paths
429
+ `{"workspaces": [...]}`; each entry uses `state_path` and `work_dir`, with optional
430
+ display/URL `id` and `enable_board` (default `true`), and is exposed at `/w/<id>/`.
431
+ `state_path` is a filename prefix: `/data/demo` uses
432
+ `/data/demo.pycodex-ws.json` for the session index,
433
+ `/data/demo.pycodex-views.json` for file views and `/data/demo.html` for Canvas.
434
+ Suffixes are appended, so prefixes containing dots remain distinct.
435
+ The resolved `state_path` is the workspace identity; duplicates are rejected
436
+ regardless of name or Canvas setting. Relative `state_path` and `work_dir` paths
331
437
  resolve from the config file directory. The root path `/` shows a workspaces
332
- management page with `add_workspace(name=None, dir="./", board=None)` and
333
- `delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
334
- etc. If `board` is omitted when adding a workspace, pycodex assigns a random
335
- writable `/tmp/pcws-*.html` board path. Add/delete actions and later
336
- session-state saves refresh the JSON file. Board HTML can reference local
438
+ management page with create/delete controls; omitted names become `workspace-1`,
439
+ `workspace-2`, etc. Omitted `state_path` when creating a workspace generates a
440
+ writable `/tmp/pcws-*` prefix. `enable_board: false` hides Canvas and disables its
441
+ routes, polling and context injection; sessions and file views still persist.
442
+ Existing `board: "demo.html"` and `board: false` configurations load with their
443
+ original state paths and are saved in the new format. See the
444
+ [workspace configuration example](packages/pycodex-ws/README.md).
445
+ Add/delete actions and later session-state saves refresh the JSON file.
446
+ Board HTML can reference local
337
447
  images beside the board (including nested paths) with relative URLs; only
338
448
  `image/*` files contained by the board directory are served.
339
449
  Assistant Markdown supports KaTeX formulas with `$...$`, `$$...$$`,
@@ -366,7 +476,7 @@ Current behavior:
366
476
  time when it expires.
367
477
  - while a background command or clock is pending, the idle status is
368
478
  `idle: sleeping`
369
- - only the active workspace tab shows its close button; closing it restores
479
+ - only the active workspace session tab shows its close button; closing it restores
370
480
  the selected tab's conversation, draft, and scroll position immediately
371
481
  after its snapshot loads
372
482
  - `--vllm-endpoint http://host:port` automatically launches a local
@@ -387,6 +497,10 @@ Current behavior:
387
497
  reasoning, retries the same downstream request once, and only then emits
388
498
  `response.failed` with `type = "model_output_invalid"` if the retry is still
389
499
  reasoning-only
500
+ - compat reasoning fragments are forwarded immediately as
501
+ `response.reasoning_text.delta`, including thinking from `/v1/messages`.
502
+ Continuous reasoning keeps the client's read idle timeout from expiring
503
+ while it waits for assistant text; only finalized reasoning items enter history
390
504
  - malformed JSON in ordinary downstream function-call arguments is detected by
391
505
  `responses_server` and returned as `response.failed` with `type = "server_error"`;
392
506
  the client's existing `stream_max_retries` policy retries the current request
@@ -421,6 +535,13 @@ pycodex doctor --json
421
535
  `Portable Mode` is the quickest way to bring your usual `pycodex` setup into a
422
536
  fresh machine, container, or debug image.
423
537
 
538
+ The storage client and server are both included in `python-codex`. To start a
539
+ local storage service, run this in a separate terminal:
540
+
541
+ ```bash
542
+ python -m pycodex.portable.server --root .tmp/pycodex_storage --host 127.0.0.1 --port 5577
543
+ ```
544
+
424
545
  Use it like this:
425
546
 
426
547
  ```bash
@@ -614,7 +735,9 @@ CLI, Web and Feishu all use `AgentRuntime.submit_input`: not just `/model`
614
735
  and `/resume`, but every session command, steer/queue admission, interactive
615
736
  questions and permissions, state notifications and shutdown. Frontends only
616
737
  adapt input and render events. Web no longer imports or runs the CLI shell.
617
- IPython is the deliberate exception: `ipython_agent()` still returns a bare Agent.
738
+ IPython is the deliberate exception: `pycodex_ws.ipython.ipython_agent()` returns
739
+ a bare Agent. Workspace assembly uses `pycodex_ws.build_runtime()` to add
740
+ `/link` and `/unlink`; `pycodex.bootstrap.build_runtime()` provides core commands.
618
741
 
619
742
  ```python
620
743
  from pycodex.bootstrap import build_agent, build_model, build_runtime
@@ -1,21 +1,16 @@
1
- pycodex/__init__.py,sha256=4BBGUXImsNBdSQjp2KtshkjQK9BMz5gK93DzpWokkWY,3235
1
+ pycodex/__init__.py,sha256=fHEyhylULeRo-5xEnl-UxT5ZpBzbFzRW7vLwdy_7oxU,2660
2
2
  pycodex/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
3
  pycodex/agent.py,sha256=zCIG2DV0oQntLXHFj8FVRkgBp5TCC0YJ7sz0lni9J1s,21177
4
- pycodex/bootstrap.py,sha256=DSVx8u94tHRYbUa_e9zdKPqozOv_CrfctbTKZ4JG064,13765
5
- pycodex/cli.py,sha256=m5IJhnodNqsfMTIzdwbuaHBl2r-N8x1MxYSpe_hlHiM,12997
6
- pycodex/compat.py,sha256=yoQQWMiAQ6ytZv3DAMIbp66ImA0kyY4G5eKHrtwmj6c,3490
7
- pycodex/context.py,sha256=nkq2Y0coZNpWs5IJj7uVVT65fDqh58Z9c07y_Wy0f4Q,26604
8
- pycodex/doctor.py,sha256=9p624b1zloXbhyMRi1-Of1YtnYeoF4zHxfGFhfujDS4,10620
9
- pycodex/events.py,sha256=MZljtqra6-Hnt9Emjbn_wgp5cGaODx1w3CbmIKd5htI,29692
10
- pycodex/feishu_card.py,sha256=Gclo4TI3KTfdT5NDLiv58CRg6hH3sUCRXuSrHgXgYjI,27767
11
- pycodex/feishu_link.py,sha256=yUbEvO-8g-Bxo8_cF9s6vLZguz3NA6q3qt2A9Q09fsE,14523
12
- pycodex/model.py,sha256=HeHeaYEC06idh_66mM4uzyc7IZc_AS789ajByvOqbr8,44571
13
- pycodex/model_metadata.py,sha256=RMYhvgkm2v1nhCslSiDXPAwrRAx8FPz9QJqv8QTbWlc,1332
14
- pycodex/portable.py,sha256=rfOpVnBZ01RS0Kyjo_w2FNKHY2t-lUcHVtWhPsbp1-k,16158
15
- pycodex/portable_server.py,sha256=A4IY3xHtBQLRxdHQ4O5zdaVGjqENaLvypsSkLGZvCZ0,7498
16
- pycodex/protocol.py,sha256=dRcTVKTaE7Eaq_jiSYdCzBMebIo3gnL7wB1x-NJqY5E,12027
17
- pycodex/runtime.py,sha256=bRlqrmXqK6kPndAG96QoGALKicbWmfuXgo2jDvbQzdE,23806
18
- pycodex/runtime_services.py,sha256=4jDCMlXHwgbJZPJkWzVEPCRHPxrD0mpCBbDInVToGG8,15033
4
+ pycodex/bootstrap.py,sha256=U1X5kwORpS5JnPk9HhIiOVWPQzDLlk_ro_cY2knwL8I,12421
5
+ pycodex/cli.py,sha256=MLvXpVwrb-HJuVWmPFmNMTR-4cqR3X08ZfAevYlVSQI,12646
6
+ pycodex/context.py,sha256=yfNY2alT3caQQwhay47pSSrGUbgEAcLKhTVaZqtQ4yU,26610
7
+ pycodex/events.py,sha256=wysYXHLiCHzW3k1yaAuoRTw90NRhHPa3jtY0ANGOqH4,29694
8
+ pycodex/model.py,sha256=3mWXTN-oV2Q50SgQYac45WT5Qk2NSmPTxSWyF-w8TRg,44583
9
+ pycodex/protocol.py,sha256=uzUz_06iA4m9vAoqZeCGiJ4SocKZDzOfCJUU4Bpt5dI,12033
10
+ pycodex/runtime.py,sha256=EM8VRfb7Mq2tj39s26lYbAFDzgzzK7qxtPS8RLenoLQ,23812
11
+ pycodex/portable/__init__.py,sha256=FmspRPmblYuZIVXMpXaxE1uosCaz2hZDpkpOzxDJQic,1084
12
+ pycodex/portable/client.py,sha256=DOxzZljVfN1ynmhqrChX7NsT1IFSFVKzEn1J3wvV3XQ,16038
13
+ pycodex/portable/server.py,sha256=w4YD71Sxb7MD5q57GPuIgx7AoicTv0R9xzX2kAaibPA,7503
19
14
  pycodex/prompts/default_base_instructions.md,sha256=D65mcj6bo4CDvVom-D9cbJRJVNquo0NghKt164_fRsg,20923
20
15
  pycodex/prompts/models.json,sha256=u-bS5xtI7ZHbrG_nbzrt-xmp5RtQT6YL8PHbd_4XZB4,397650
21
16
  pycodex/prompts/permissions/approval_policy/never.md,sha256=QceTG6wjkaJARjYr0HYV1aPnPcpGcrkRUW-smWRr6MQ,120
@@ -29,64 +24,67 @@ pycodex/prompts/permissions/sandbox_mode/workspace_write.md,sha256=lVN-LwrBbHqlv
29
24
  pycodex/tools/__init__.py,sha256=UYwmsX_Ckw_YaYQ6TYhUU6w5pxi2qzZoTrq6BAgNErM,1857
30
25
  pycodex/tools/agent_tool_schemas.py,sha256=8cL9ml4So0H6IIeK0ZzjAo9bk3VeprDOSYPpIamOqEU,2048
31
26
  pycodex/tools/apply_patch_tool.py,sha256=t8XFCGZ7q-s9C1Ty6qkReBRk-QD3ONvcKXLAz9djKRY,13956
32
- pycodex/tools/base_tool.py,sha256=kNNzPWijtdl4C_GunyR5G1qvsr7KQtnFJ2RiRHHnAYo,6478
27
+ pycodex/tools/base_tool.py,sha256=Oqe-0RgH3mz70YckgBFD-tdvNJ9TQXrb0tuMbdy3bfc,6477
33
28
  pycodex/tools/clock_tool.py,sha256=2_RYbBEw4V9_f1L-qZl3hhJ6XNPM_gBDmOlCh1cCAjo,6575
34
- pycodex/tools/close_agent_tool.py,sha256=zQtvd5iGW6YCPFiec8zc2OdGtgNIxoA4p2ZTuCoJOaI,2031
35
- pycodex/tools/code_mode_manager.py,sha256=VFVn7S_qUHCOHW952CMeGxTIE-7CPOx2uuXJZgBml8o,19160
29
+ pycodex/tools/close_agent_tool.py,sha256=uIbCjsQx9MLkjVYheS8ZVf5VVPbC6Iviwox4pneJpmI,2030
30
+ pycodex/tools/code_mode_manager.py,sha256=HUgtXxj3hGj2i4_tsToHLr8mGTV8uIob9vtYAZeLnQc,19166
36
31
  pycodex/tools/exec_command_tool.py,sha256=iqRiKaaX-AjG6LjATAMOakHIP0gpCWCj2o8af_8eAiQ,4464
37
32
  pycodex/tools/exec_runtime.js,sha256=DR1uocKailTqNWAcJNFJuQgFFMSUzTpT_uQsRaneg2s,3643
38
33
  pycodex/tools/exec_tool.py,sha256=tuC21rlhP8CPSYml4XnSDSRV0UPGkQ6aHj0DLIoOOvM,2274
39
34
  pycodex/tools/grep_files_tool.py,sha256=Ysw68VKI9OEvUH5haYx8FWuthkHykc1wskbuzjj8gTk,5293
40
- pycodex/tools/ipython_tool.py,sha256=SKVYpJEwlYjOQZQWbpqvrA-3K9aJTlIG9KM50p5JYUM,4496
41
35
  pycodex/tools/list_dir_tool.py,sha256=_U6ouRcVbGHT5mChVyUsiMgDGrFK7EFMFaspPLAHtH8,5337
42
36
  pycodex/tools/read_file_tool.py,sha256=YK__uhIVldBjAQega5S_ZuZU5jgP_kLOwr-YhjAmRo4,9735
43
- pycodex/tools/request_permissions_tool.py,sha256=n8eg29SgRbx1DUxxLbAztnc5i-0j2N2WnDupspjwWbE,3979
37
+ pycodex/tools/request_permissions_tool.py,sha256=nEt3_uw3lmFXyydqcN3PxuKwrEvaRPtfjA_U8nikCtU,3978
44
38
  pycodex/tools/request_user_input_tool.py,sha256=_7siHF2p3KiNDFwsTqKKIUw2kPCXMzLaR12PkZ1wdIQ,4050
45
- pycodex/tools/resume_agent_tool.py,sha256=D_OcWQbIbsWf-2NrRgDab4hB6BUbay0XidfPInhg88Y,1614
46
- pycodex/tools/send_input_tool.py,sha256=J91LLz4RpvHmBWti0XZgI9xWipWwD8-1hQIdC8-Jd5E,3762
39
+ pycodex/tools/resume_agent_tool.py,sha256=bZvXEUNdc1RVD2N5Msjv63Rni62FQk4gj9LAXK1Vsy8,1613
40
+ pycodex/tools/runtime_services.py,sha256=vZHIHGAeTA-DoEoHulAKvjcfHLXkuK1t3T0Mob9VJ8A,15044
41
+ pycodex/tools/send_input_tool.py,sha256=w63SKM05_e3oXvdaBtL-hk941TvkWKuTUDEqTSyWEMQ,3761
47
42
  pycodex/tools/shell_command_tool.py,sha256=US63I5xiV5zfO1Oox2yHUg7F6XoKzmyxtohfaQIsmrw,4216
48
43
  pycodex/tools/shell_tool.py,sha256=lqFH2XosfBg7yQKhpB7i4vLGz9sNQtEZihD0rjsmBCQ,4068
49
- pycodex/tools/spawn_agent_tool.py,sha256=n_WZi1IX4Waj-GQ-hR1BVlk3zzw_ssvem7zcj3tYI38,6209
50
- pycodex/tools/unified_exec_manager.py,sha256=sO1H3uoXyZ6DiyF98-eidQ77mS-P5gSWyPqM3L50yTQ,14538
51
- pycodex/tools/update_plan_tool.py,sha256=8zyANrQgj8u2LBJ2yz_0Cg5jP3S6ibp6eASxUupX9C8,3199
52
- pycodex/tools/view_image_tool.py,sha256=UZRkBEwuCkqxOw7XBwacdgPZAtbnD6uSHTdm-6amUo4,3902
53
- pycodex/tools/wait_agent_tool.py,sha256=2OWfb9FCGu5SjbRp33dVEJWG3apMtQgczvQR1V_rm6I,3829
44
+ pycodex/tools/spawn_agent_tool.py,sha256=Jx5jYpxsH_CQ3BLkXYPZuIINW5deO_zY8tyw8kZpreY,6208
45
+ pycodex/tools/unified_exec_manager.py,sha256=XPN6PgBnjZ5WFZATNIqpWKUAuw8HMMD7mlfHJQjJQZM,14544
46
+ pycodex/tools/update_plan_tool.py,sha256=xwrSrerw5qfkbFc9Ltx1OZEmD__CWtryOy3UhL0-Jp4,3198
47
+ pycodex/tools/view_image_tool.py,sha256=SCoiIHaQ51oYjYX69MRxjAK9b1P6_wOCIuZqzsC2rj8,4082
48
+ pycodex/tools/wait_agent_tool.py,sha256=9Os6RzzCxIU5KJI5l6MzdwO6QKLNxLFwFXGeDMfYlP4,3828
54
49
  pycodex/tools/wait_tool.py,sha256=0_OO_DvMsBhG3oDKZePTOjZTBq1lmWntXAzqIqvebdA,3206
55
50
  pycodex/tools/web_search_tool.py,sha256=3ao1IvJw7MwcP35UKLbqrM-nlm5Z-UUFVdEzkJYOV1o,988
56
51
  pycodex/tools/write_stdin_tool.py,sha256=jvBDchLLmoDITd7g-ZVeEVXHRhWKkmnjM5laL-53FOM,3609
57
- pycodex/utils/__init__.py,sha256=lbf-jbo6JS7t1wfFkQqLHlG4zZVqXYGkOIj2cvtrjx8,591
52
+ pycodex/utils/__init__.py,sha256=ih_KyZ2_bajR5YlZ1QMTWOHnuEBda-S3gQ7PwnBtLZQ,453
58
53
  pycodex/utils/async_bridge.py,sha256=d21Pjim-nsQbSG5pJddd0WaQ03CzA3w3TINWDmmjWbg,1815
59
54
  pycodex/utils/compactor.py,sha256=0tlszw1n54aMEGH6sL1U1dEgFSU5ioYL2NI5uSvHSiM,6119
60
- pycodex/utils/debug.py,sha256=BdXmuAQVWVB1BONKRDlkMpgXpkt4M1jhvEAq5n2AumI,296
55
+ pycodex/utils/compat.py,sha256=QxcI4AQuUD-gE2494qPGezgm0OIX98qSHdAiof1JrlU,3550
56
+ pycodex/utils/crypto.py,sha256=HRCXF26Ft3S8CG2X0YpZwa8GMU9YJ0Loo8Q9PMPMcNw,1249
57
+ pycodex/utils/debug.py,sha256=5XMpWpwFSm6woyx6RGXvN2qL0afRtTQbwlOAv-LotTw,913
58
+ pycodex/utils/doctor.py,sha256=fI66oQqB5zNL7aEpFbiHnzhELyp9XydwMyAGznw5YSE,10616
61
59
  pycodex/utils/dotenv.py,sha256=h7ZgvqpEswetqjhtwfiaHyokAHwNKPakizTAHPHz5Ds,1865
62
60
  pycodex/utils/event_helpers.py,sha256=pnxL0-nBC7cnOheHPCIfkHqJjMbkJg3C348_EzAVhFA,6342
63
61
  pycodex/utils/get_env.py,sha256=L4S8h84tkgxZKvrxY7ezTgePbXepG-yIigUTKeY6uuE,6094
64
- pycodex/utils/image_utils.py,sha256=1kXhtTqL4xtazpEABHhtvJBUJlceo8hSzg6ukmf_4FA,2407
62
+ pycodex/utils/image_utils.py,sha256=OUzUs0Cow4L8Z2np0EBeEUotAqqsCmRmhrV8AxZUXRw,2460
63
+ pycodex/utils/model_metadata.py,sha256=uK3iI4yC7Ge7yN4I1_gQYalQ4MYKddz2qMwsmQb-P80,1337
65
64
  pycodex/utils/random_ids.py,sha256=jLTxZZ0MBbRdQOFDFWGVRjWzIo6FKSQ5ADXbQSLPp1s,379
66
65
  pycodex/utils/session_persist.py,sha256=v9KhmjUB2yCg55c85i8ZU1BWfc-WEMDPmpT0Rv4K8a8,22489
67
66
  pycodex/utils/truncation.py,sha256=5D6vAorT8EgXgjd7ASwtraAyIZdlCR-w5jKZqJxDzho,5446
67
+ pycodex/utils/_aes_gcm/LICENSE,sha256=KvdHagY3sYIK34GldJaRi34sNnGwMVJIIoDhbC9MRu8,2805
68
+ pycodex/utils/_aes_gcm/README.md,sha256=D9XGeewCgU2PVqJCO6WusUs0Mw2FbMnABCqaVktqCnE,3045
69
+ pycodex/utils/_aes_gcm/__init__.py,sha256=p0U1CVy6dQBDAfGvSYG0opn6iP2VGF5rqUUM-mNLJxU,70
70
+ pycodex/utils/_aes_gcm/aes.py,sha256=12p_5O7gX-e-LQ08nNe2CBdP_H4eX3J84LlXaVr9i8A,9269
71
+ pycodex/utils/_aes_gcm/gcm.py,sha256=Z8fYHpnLpqJyTPmjDOZtryqe0xmT_93vscNuJe5ZY-w,2762
72
+ pycodex/utils/_aes_gcm/source-sha256.json,sha256=g9uPWXuA0D9Wq9EGnduyiVanTgi8QWRFW-9T69FG-fc,215
68
73
  responses_server/__init__.py,sha256=sx9C88YWqmehASZy24FUMqT14A36doM_rj0MIZtQ9iQ,377
69
74
  responses_server/__main__.py,sha256=876xZTqhPeDBD2GKKr8pKX33Af8PhFTosAI8bYQsFgQ,61
70
- responses_server/app.py,sha256=9uhglqUaRXhotYDkGFemnETopgx9Moi2wHOE0pDCwtU,7755
75
+ responses_server/app.py,sha256=PE-jWn_hmL5os1xsVL81KwU0kDZHOkZoVdSOK-7x3xQ,8400
71
76
  responses_server/config.py,sha256=sCJYGisPHW9Uup26-R9Zv0o9W_kRVt-ETc2MFCVjG90,2795
72
77
  responses_server/messages_api.py,sha256=ialAYTMmN0y-uxQB3Y51ukq-kvkSVO84WVLXaqLsKw0,18775
73
78
  responses_server/payload_processors.py,sha256=dqPwqHD-FhyiIwjzFEMf2wCtQDxkRyerZzbdqnH1aDE,3561
74
79
  responses_server/server.py,sha256=k4DbsOYkaHHcy38uF9u_QXahJC1cplKsTu-MdC3DCIs,2286
75
80
  responses_server/session_store.py,sha256=nwGxQL6nUVKnDVeDHz8IzqNQ7dWk8Ybf0URekOHdxTo,1133
76
- responses_server/stream_router.py,sha256=Bwuz_a_eEys7OuuN5qgtCIQF0mIo7DWDNojLI5mNetE,46062
81
+ responses_server/stream_router.py,sha256=TQX2-8uOGad3TBJnRu9SOtDaHBHjYwE-WERYz6rcuo8,46274
77
82
  responses_server/trajectory_dump.py,sha256=5_Wy6tslszrqx3j1JeP_boyTbzIicKy8No84PzMnmHk,5111
78
83
  responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
79
84
  responses_server/tools/custom_adapter.py,sha256=SnIiNUdomff3APtTriURAC9YCeEdhCDjzGMEYusfUdk,8340
80
85
  responses_server/tools/web_search.py,sha256=pnKH9JyTScRehWpZS3HpXRvjWsQvC0QZqxjEOCT4ndg,8936
81
- workspace_server/__init__.py,sha256=PRM4ONODb6hxjBUHkBD4TprE_pkTCIlT9sfftK5ic6M,866
82
- workspace_server/__main__.py,sha256=876xZTqhPeDBD2GKKr8pKX33Af8PhFTosAI8bYQsFgQ,61
83
- workspace_server/app.py,sha256=oYvUFxTGurXN0PDP_opUePyW_tBE3SauiSma_U4cc9c,59260
84
- workspace_server/owned_process.py,sha256=j1cJ89Nm7gS91Zj62kYKabFbb9qhbU7-q1YDsWcVcn8,895
85
- workspace_server/workspace.html,sha256=CXrMdUVM9eB03nCVwvC5coc6tMsmplOOx8wLBrJc9cw,71625
86
- workspace_server/workspaces.html,sha256=TpmfzpuCIXTVdFQzQrojxSCtBTHMSESn1zNGOmNz_W8,15787
87
- workspace_server/workspaces.py,sha256=cyyDBMo0UVqMvCjeAEHNwZCHQzRRsyn7bbYIb0INOo0,22371
88
- python_codex-0.3.2.dist-info/METADATA,sha256=PClQOhAVKTGw8gaoYjP_G2pfp6j5QEZQS8uHPccymqg,39624
89
- python_codex-0.3.2.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
90
- python_codex-0.3.2.dist-info/entry_points.txt,sha256=vkV2UWCtEKvQNMJuPNjt8HyBKiwp83JyqBatrBNGDp8,80
91
- python_codex-0.3.2.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
92
- python_codex-0.3.2.dist-info/RECORD,,
86
+ python_codex-0.4.0.dist-info/METADATA,sha256=nb46V9WoOekFjc7X3Ep9U0NNoV7bf7DK8Io12_Uch8s,47415
87
+ python_codex-0.4.0.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
88
+ python_codex-0.4.0.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
89
+ python_codex-0.4.0.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
90
+ python_codex-0.4.0.dist-info/RECORD,,
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  pycodex = pycodex.cli:main
3
- pycodex-ws = workspace_server:main
responses_server/app.py CHANGED
@@ -9,8 +9,11 @@ from dataclasses import replace
9
9
  from typing import Iterator
10
10
 
11
11
  import uvicorn
12
- from fastapi import FastAPI, Request
13
- from fastapi.responses import JSONResponse, StreamingResponse
12
+ from starlette.applications import Starlette
13
+ from starlette.exceptions import HTTPException
14
+ from starlette.requests import Request
15
+ from starlette.responses import JSONResponse, StreamingResponse
16
+ from starlette.routing import Route
14
17
 
15
18
  from .config import CompatServerConfig
16
19
  from .server import ResponseServer
@@ -22,6 +25,12 @@ def _run_uvicorn_server(server) -> "None":
22
25
  server.run()
23
26
 
24
27
 
28
+ async def _http_exception_response(request: "Request", exc: "HTTPException"):
29
+ return JSONResponse(
30
+ {"detail": exc.detail}, status_code=exc.status_code, headers=exc.headers
31
+ )
32
+
33
+
25
34
  def _format_sse_event(
26
35
  event_name: "str", payload: "typing.Dict[str, object]"
27
36
  ) -> "bytes":
@@ -113,33 +122,25 @@ class ManagedResponseServer:
113
122
  config: "CompatServerConfig",
114
123
  session_store=None,
115
124
  stream_router=None,
116
- ) -> "FastAPI":
125
+ ) -> "Starlette":
117
126
  response_server = ResponseServer(
118
127
  config,
119
128
  session_store=session_store,
120
129
  stream_router=stream_router,
121
130
  )
122
- app = FastAPI(title="ResponsesCompat", version="0.1.0")
123
- app.state.response_server = response_server
124
131
 
125
- @app.get("/health")
126
- @app.get("/healthz")
127
- async def health() -> "typing.Dict[str, bool]":
128
- return {"ok": True}
132
+ async def health(request: "Request"):
133
+ return JSONResponse({"ok": True})
129
134
 
130
- @app.get("/models")
131
- @app.get("/v1/models")
132
- async def list_models():
135
+ async def list_models(request: "Request"):
133
136
  try:
134
- return response_server.list_models()
137
+ return JSONResponse(response_server.list_models())
135
138
  except OutcommingChatError as exc:
136
139
  return JSONResponse(
137
140
  {"error": {"message": str(exc)}},
138
141
  status_code=502,
139
142
  )
140
143
 
141
- @app.post("/responses")
142
- @app.post("/v1/responses")
143
144
  async def responses(request: "Request"):
144
145
  try:
145
146
  request_body = await request.json()
@@ -174,6 +175,18 @@ class ManagedResponseServer:
174
175
  },
175
176
  )
176
177
 
178
+ app = Starlette(
179
+ routes=[
180
+ Route("/health", health, methods=["GET"]),
181
+ Route("/healthz", health, methods=["GET"]),
182
+ Route("/models", list_models, methods=["GET"]),
183
+ Route("/v1/models", list_models, methods=["GET"]),
184
+ Route("/responses", responses, methods=["POST"]),
185
+ Route("/v1/responses", responses, methods=["POST"]),
186
+ ],
187
+ exception_handlers={HTTPException: _http_exception_response},
188
+ )
189
+ app.state.response_server = response_server
177
190
  return app
178
191
 
179
192
  def __init__(self, config: "CompatServerConfig") -> "None":
@@ -894,19 +894,23 @@ class StreamRouter:
894
894
 
895
895
  reasoning = delta.get("reasoning")
896
896
  reasoning_content = delta.get("reasoning_content")
897
- if (
898
- isinstance(reasoning, str)
899
- and reasoning
900
- and isinstance(reasoning_content, str)
901
- and reasoning == reasoning_content
902
- ):
903
- # Some chat providers expose the same delta under both aliases.
904
- reasoning_parts.append(reasoning)
905
- else:
906
- if isinstance(reasoning, str) and reasoning:
907
- reasoning_parts.append(reasoning)
908
- if isinstance(reasoning_content, str) and reasoning_content:
909
- reasoning_parts.append(reasoning_content)
897
+ reasoning_deltas = [reasoning]
898
+ # Some chat providers expose the same delta under both aliases.
899
+ if reasoning_content != reasoning:
900
+ reasoning_deltas.append(reasoning_content)
901
+ for reasoning_delta in reasoning_deltas:
902
+ if isinstance(reasoning_delta, str) and reasoning_delta:
903
+ reasoning_parts.append(reasoning_delta)
904
+ events.append(
905
+ (
906
+ "response.reasoning_text.delta",
907
+ {
908
+ "type": "response.reasoning_text.delta",
909
+ "content_index": 0,
910
+ "delta": reasoning_delta,
911
+ },
912
+ )
913
+ )
910
914
 
911
915
  content = delta.get("content")
912
916
  if isinstance(content, str) and content: