python-codex 0.2.4__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 (124) hide show
  1. {python_codex-0.2.4 → python_codex-0.2.5}/AGENTS.md +2 -0
  2. {python_codex-0.2.4 → python_codex-0.2.5}/PKG-INFO +16 -4
  3. {python_codex-0.2.4 → python_codex-0.2.5}/README.md +15 -3
  4. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/context.py +4 -18
  5. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/interactive_session.py +15 -1
  6. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/model.py +153 -11
  7. python_codex-0.2.5/pycodex/model_metadata.py +31 -0
  8. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/models.json +334 -0
  9. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/unified_exec_manager.py +32 -1
  10. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/compactor.py +5 -87
  11. {python_codex-0.2.4 → python_codex-0.2.5}/pyproject.toml +1 -1
  12. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_agent.py +7 -14
  13. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_builtin_tools.py +58 -0
  14. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_cli.py +32 -5
  15. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_compactor.py +9 -9
  16. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_context.py +10 -0
  17. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_model.py +186 -0
  18. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_workspace_server.py +155 -0
  19. {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/app.py +57 -1
  20. {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/workspace.html +44 -1
  21. {python_codex-0.2.4 → python_codex-0.2.5}/.github/workflows/publish.yml +0 -0
  22. {python_codex-0.2.4 → python_codex-0.2.5}/.github/workflows/test.yml +0 -0
  23. {python_codex-0.2.4 → python_codex-0.2.5}/.gitignore +0 -0
  24. {python_codex-0.2.4 → python_codex-0.2.5}/LICENSE +0 -0
  25. {python_codex-0.2.4 → python_codex-0.2.5}/README_ZH.md +0 -0
  26. {python_codex-0.2.4 → python_codex-0.2.5}/docs/ALIGNMENT.md +0 -0
  27. {python_codex-0.2.4 → python_codex-0.2.5}/docs/CONTEXT.md +0 -0
  28. {python_codex-0.2.4 → python_codex-0.2.5}/docs/responses_server/README.md +0 -0
  29. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/__init__.py +0 -0
  30. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/agent.py +0 -0
  31. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/cli.py +0 -0
  32. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/collaboration.py +0 -0
  33. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/compat.py +0 -0
  34. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/doctor.py +0 -0
  35. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/feishu_card.py +0 -0
  36. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/feishu_link.py +0 -0
  37. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/portable.py +0 -0
  38. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/portable_server.py +0 -0
  39. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/collaboration_default.md +0 -0
  40. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/collaboration_plan.md +0 -0
  41. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/default_base_instructions.md +0 -0
  42. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
  43. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
  44. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
  45. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
  46. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
  47. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
  48. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
  49. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
  50. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/protocol.py +0 -0
  51. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/runtime.py +0 -0
  52. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/runtime_services.py +0 -0
  53. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/__init__.py +0 -0
  54. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/agent_tool_schemas.py +0 -0
  55. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/apply_patch_tool.py +0 -0
  56. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/base_tool.py +0 -0
  57. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/close_agent_tool.py +0 -0
  58. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/code_mode_manager.py +0 -0
  59. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/exec_command_tool.py +0 -0
  60. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/exec_runtime.js +0 -0
  61. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/exec_tool.py +0 -0
  62. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/grep_files_tool.py +0 -0
  63. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/ipython_tool.py +0 -0
  64. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/list_dir_tool.py +0 -0
  65. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/read_file_tool.py +0 -0
  66. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/request_permissions_tool.py +0 -0
  67. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/request_user_input_tool.py +0 -0
  68. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/resume_agent_tool.py +0 -0
  69. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/send_input_tool.py +0 -0
  70. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/shell_command_tool.py +0 -0
  71. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/shell_tool.py +0 -0
  72. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/spawn_agent_tool.py +0 -0
  73. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/update_plan_tool.py +0 -0
  74. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/view_image_tool.py +0 -0
  75. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/wait_agent_tool.py +0 -0
  76. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/wait_tool.py +0 -0
  77. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/web_search_tool.py +0 -0
  78. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/write_stdin_tool.py +0 -0
  79. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/__init__.py +0 -0
  80. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/async_bridge.py +0 -0
  81. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/debug.py +0 -0
  82. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/dotenv.py +0 -0
  83. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/get_env.py +0 -0
  84. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/random_ids.py +0 -0
  85. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/session_persist.py +0 -0
  86. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/toolcall_visualize.py +0 -0
  87. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/truncation.py +0 -0
  88. {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/visualize.py +0 -0
  89. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/__init__.py +0 -0
  90. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/__main__.py +0 -0
  91. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/app.py +0 -0
  92. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/config.py +0 -0
  93. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/messages_api.py +0 -0
  94. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/payload_processors.py +0 -0
  95. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/server.py +0 -0
  96. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/session_store.py +0 -0
  97. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/stream_router.py +0 -0
  98. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/tools/__init__.py +0 -0
  99. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/tools/custom_adapter.py +0 -0
  100. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/tools/web_search.py +0 -0
  101. {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/trajectory_dump.py +0 -0
  102. {python_codex-0.2.4 → python_codex-0.2.5}/tests/TESTS.md +0 -0
  103. {python_codex-0.2.4 → python_codex-0.2.5}/tests/__init__.py +0 -0
  104. {python_codex-0.2.4 → python_codex-0.2.5}/tests/compare_request_user_input_roundtrip.py +0 -0
  105. {python_codex-0.2.4 → python_codex-0.2.5}/tests/compare_steer_request_bodies.py +0 -0
  106. {python_codex-0.2.4 → python_codex-0.2.5}/tests/compare_tool_schemas.py +0 -0
  107. {python_codex-0.2.4 → python_codex-0.2.5}/tests/fake_responses_server.py +0 -0
  108. {python_codex-0.2.4 → python_codex-0.2.5}/tests/fakes.py +0 -0
  109. {python_codex-0.2.4 → python_codex-0.2.5}/tests/responses_server/fake_chat_completions_server.py +0 -0
  110. {python_codex-0.2.4 → python_codex-0.2.5}/tests/responses_server/test_server.py +0 -0
  111. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_async_bridge.py +0 -0
  112. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_doctor.py +0 -0
  113. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_fake_responses_server.py +0 -0
  114. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_feishu_card.py +0 -0
  115. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_feishu_link.py +0 -0
  116. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_ipython_tool.py +0 -0
  117. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_portable.py +0 -0
  118. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_py36_syntax.py +0 -0
  119. {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_visualize.py +0 -0
  120. {python_codex-0.2.4 → python_codex-0.2.5}/tools/feishu_oauth.py +0 -0
  121. {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/__init__.py +0 -0
  122. {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/__main__.py +0 -0
  123. {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/workspaces.html +0 -0
  124. {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/workspaces.py +0 -0
@@ -19,6 +19,7 @@
19
19
  - 对 vLLM chat-completions 打开 `return_token_ids=true` 时,streaming `prompt_token_ids` 只出现在首个 chunk,后续每个 chunk 的 `choices[*].token_ids` 都是 decode delta;要在 `responses_server` 侧导出 trajectory 时,按“首个 `prompt_token_ids` + 按序拼接所有 chunk 的 `token_ids`”重建即可。
20
20
  - `pycodex` 默认是最小交互 CLI;无 prompt 时进入 REPL,并通过 `AgentRuntime` 跑外层提交循环。当前会显示最小事件流、assistant 流式输出、简单 title/history(`/title`, `/history`),并默认注册一组与原版一一对应的本地工具子集。
21
21
  - Web workspace lives in the standalone `workspace_server/` package and is launched with `pycodex-ws --listen <host:port> --board <html>`, not through `pycodex` CLI dispatch. CLI and web share `pycodex.interactive_session.run_interactive_session`; slash-command semantics such as `/resume`, `/compact`, `/model`, and `/link` belong to that shared interactive shell loop, while workspace only supplies a web view/input adapter and tab/session lifecycle.
22
+ - Relative images in board HTML resolve to HTTP siblings such as `/w/<workspace-id>/<image>` (or `/<image>` in single-workspace mode), not to `/board/...`. Keep the image fallback routes after concrete workspace/API routes, resolve files from the board's directory, and reject non-`image/*` files or paths that escape that directory.
22
23
  - 交互 CLI 的事件流展示优先表达用户可感知的阶段(例如工具开始/完成、模型回看工具结果),不要直接把内部 `iteration` 计数暴露成主要状态文案;`iterations` 应继续保留在 `TurnResult` 等程序化结果里。
23
24
  - 在交互 CLI 里,`stream_error` 表示当前 Responses stream attempt 失败且模型客户端可能马上自动重试;不要在这个事件上 `finish_stream()` 输出当前 assistant delta buffer,否则第一次失败 attempt 的文本和重试成功后的最终回复会重复显示。真正 fatal 的失败仍由 `turn_failed` 走通用 flush,保留 partial 输出。
24
25
  - prompt/context 相关逻辑统一放在 `pycodex/context.py`:`AgentLoop` 只维护真实会话历史;每轮请求前由 `ContextManager` 注入 base instructions、developer message、`AGENTS.md` 指令和 `<environment_context>`,且这些注入项不写回 history。
@@ -61,6 +62,7 @@
61
62
  - auto-compact 对齐上游配置名 `model_auto_compact_token_limit`;为空时关闭,触发依据是最近一次模型上报的 `usage.total_tokens`,pre-turn 压缩上一轮历史,mid-turn 压缩工具 follow-up 前的当前历史,并继续复用现有 compacted rollout 记录。
62
63
  - Responses streaming 里的 `response.incomplete` 不是连接断开:不要让 `ResponsesModelClient` 把它当 retryable incomplete stream 反复重连。普通 turn 应明确报 `response.incomplete`;compact 请求如果已经收到 assistant partial summary,可以用这个 partial summary 完成 replacement history,避免 midturn auto-compact 卡在 5 次 retry。
63
64
  - 上游 Codex Responses 请求当前不传模型级 `max_output_tokens`,也没有读取 `model_max_output_tokens` 这个 config key;这个名字在上游主要用于工具输出截断,不要为了上游对齐把它加进模型请求。
65
+ - `service_tier = "fast"` 在 config 中是 Fast mode 的持久写法,但 Responses wire 值是 `priority`;只对模型 metadata 的 `service_tiers` 明确包含该 id 的模型发送,`default` 和不支持的 tier 都省略。
64
66
  - 普通 turn 遇到 `ResponsesIncompleteError(reason="max_output_tokens")` 时,上游语义是保留异常前已经收到的 `response.output_item.done`;pycodex 因为模型客户端按整轮返回,需要在异常路径把这些 done assistant/reasoning items 写入 history 和 rollout,才能让用户下一句 `continue` 接上。不要把纯 `response.output_text.delta` 合成 history,也不要持久化没有 tool result 的 tool call。
65
67
  - Feishu card tests read `~/.codex/.feishu_refresh_token` through production code; when running `tests/test_feishu_card.py` locally, isolate HOME (for example `HOME=/tmp/pycodex-empty-home env -u VIRTUAL_ENV uv run pytest tests/test_feishu_card.py tests/test_feishu_link.py`) unless the test itself controls `HOME`.
66
68
  - `lark_oapi.ws.client` creates a module-level asyncio `loop` at import time and `Client.start()` always uses that global. For `/link` long-connection listeners, bind that SDK global to a listener-thread-owned loop before constructing the client, and stop it through private `_disconnect()` plus `loop.stop()` on `/unlink`; otherwise unlink/link can reuse a still-running SDK loop and fail with `RuntimeError: This event loop is already running`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.2.4
3
+ Version: 0.2.5
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
@@ -179,7 +179,8 @@ Current behavior:
179
179
  - interactive mode shows a compact event stream for user-visible phases such as
180
180
  tool execution and model follow-up after tool results
181
181
  - assistant text is printed from streaming deltas directly
182
- - interactive mode supports `/history`, `/title`, `/model`, `/resume`, and `/compact`
182
+ - interactive mode supports `/history`, `/title`, `/model`, `/resume`, `/compact`,
183
+ and `/fork`
183
184
  - `/model <name>` switches the model used by later turns in the current
184
185
  interactive session; `/model` shows the current model and available choices
185
186
  - `/resume` with no argument lists the currently resumable sessions by their
@@ -189,9 +190,15 @@ Current behavior:
189
190
  - `/compact` synthesizes a local handoff summary, replaces the in-memory
190
191
  conversation history with the compacted view, and appends a compacted-history
191
192
  entry to the rollout so later `/resume` sees the same state
193
+ - `/fork` generates a new model session id while preserving the current history,
194
+ rollout, and workspace tab
192
195
  - `model_auto_compact_token_limit = <tokens>` in `config.toml` enables the same
193
196
  compaction path automatically when the latest reported usage reaches that
194
197
  threshold before a follow-up sampling request or the next user turn
198
+ - `service_tier = "fast"` enables Fast mode for models whose vendored metadata
199
+ advertises the `priority` service tier; pycodex follows upstream Codex by
200
+ sending `service_tier = "priority"` on the Responses request, while
201
+ `service_tier = "default"` or unsupported tiers are omitted
195
202
  - if a model request fails with `context_length_exceeded`, pycodex now treats
196
203
  the provider-reported requested token count as a failed-request usage sample,
197
204
  triggers the same compact path immediately, and retries the request once; if
@@ -219,8 +226,13 @@ Current behavior:
219
226
  `delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
220
227
  etc. If `board` is omitted when adding a workspace, pycodex assigns a random
221
228
  writable `/tmp/pcws-*.html` board path. Add/delete actions and later
222
- session-state saves refresh the JSON file. `--password <value>` enables a
223
- password-only login page for workspace pages, APIs, and websocket connections.
229
+ session-state saves refresh the JSON file. Board HTML can reference local
230
+ images beside the board (including nested paths) with relative URLs; only
231
+ `image/*` files contained by the board directory are served.
232
+ Assistant Markdown supports KaTeX formulas with `$...$`, `$$...$$`,
233
+ `\(...\)`, and `\[...\]` delimiters.
234
+ `--password <value>` enables a password-only login page for workspace pages,
235
+ APIs, and websocket connections.
224
236
  - steer is enabled by default in interactive mode: normal input goes into the
225
237
  runtime steer path, the current request stops at the next safe boundary, and
226
238
  later steer text is appended to the next model request's `input` in order;
@@ -158,7 +158,8 @@ Current behavior:
158
158
  - interactive mode shows a compact event stream for user-visible phases such as
159
159
  tool execution and model follow-up after tool results
160
160
  - assistant text is printed from streaming deltas directly
161
- - interactive mode supports `/history`, `/title`, `/model`, `/resume`, and `/compact`
161
+ - interactive mode supports `/history`, `/title`, `/model`, `/resume`, `/compact`,
162
+ and `/fork`
162
163
  - `/model <name>` switches the model used by later turns in the current
163
164
  interactive session; `/model` shows the current model and available choices
164
165
  - `/resume` with no argument lists the currently resumable sessions by their
@@ -168,9 +169,15 @@ Current behavior:
168
169
  - `/compact` synthesizes a local handoff summary, replaces the in-memory
169
170
  conversation history with the compacted view, and appends a compacted-history
170
171
  entry to the rollout so later `/resume` sees the same state
172
+ - `/fork` generates a new model session id while preserving the current history,
173
+ rollout, and workspace tab
171
174
  - `model_auto_compact_token_limit = <tokens>` in `config.toml` enables the same
172
175
  compaction path automatically when the latest reported usage reaches that
173
176
  threshold before a follow-up sampling request or the next user turn
177
+ - `service_tier = "fast"` enables Fast mode for models whose vendored metadata
178
+ advertises the `priority` service tier; pycodex follows upstream Codex by
179
+ sending `service_tier = "priority"` on the Responses request, while
180
+ `service_tier = "default"` or unsupported tiers are omitted
174
181
  - if a model request fails with `context_length_exceeded`, pycodex now treats
175
182
  the provider-reported requested token count as a failed-request usage sample,
176
183
  triggers the same compact path immediately, and retries the request once; if
@@ -198,8 +205,13 @@ Current behavior:
198
205
  `delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
199
206
  etc. If `board` is omitted when adding a workspace, pycodex assigns a random
200
207
  writable `/tmp/pcws-*.html` board path. Add/delete actions and later
201
- session-state saves refresh the JSON file. `--password <value>` enables a
202
- password-only login page for workspace pages, APIs, and websocket connections.
208
+ session-state saves refresh the JSON file. Board HTML can reference local
209
+ images beside the board (including nested paths) with relative URLs; only
210
+ `image/*` files contained by the board directory are served.
211
+ Assistant Markdown supports KaTeX formulas with `$...$`, `$$...$$`,
212
+ `\(...\)`, and `\[...\]` delimiters.
213
+ `--password <value>` enables a password-only login page for workspace pages,
214
+ APIs, and websocket connections.
203
215
  - steer is enabled by default in interactive mode: normal input goes into the
204
216
  runtime steer path, the current request stops at the next safe boundary, and
205
217
  later steer text is appended to the next model request's `input` in order;
@@ -1,8 +1,6 @@
1
1
 
2
2
  from dataclasses import dataclass
3
3
  from datetime import datetime
4
- from functools import lru_cache
5
- import json
6
4
  from pathlib import Path
7
5
  import typing
8
6
 
@@ -12,6 +10,7 @@ except ModuleNotFoundError: # pragma: no cover - Python 3.10 path
12
10
  import tomli as tomllib
13
11
 
14
12
  from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
13
+ from .model_metadata import load_models_by_slug
15
14
  from .protocol import ContextMessage, ConversationItem, JSONDict, Prompt, ToolSpec
16
15
  from .utils.get_env import (
17
16
  get_sandbox_tag,
@@ -23,7 +22,6 @@ from .utils.get_env import (
23
22
  DEFAULT_BASE_INSTRUCTIONS_PATH = (
24
23
  Path(__file__).resolve().parent / "prompts" / "default_base_instructions.md"
25
24
  )
26
- DEFAULT_MODELS_PATH = Path(__file__).resolve().parent / "prompts" / "models.json"
27
25
  DEFAULT_COLLABORATION_INSTRUCTIONS_PATH = (
28
26
  Path(__file__).resolve().parent / "prompts" / "collaboration_default.md"
29
27
  )
@@ -270,7 +268,7 @@ class ContextManager:
270
268
  model_metadata = None
271
269
  model_slug = self._config.model
272
270
  if model_slug is not None:
273
- model_metadata = _load_models_by_slug().get(model_slug)
271
+ model_metadata = load_models_by_slug().get(model_slug)
274
272
 
275
273
  context_window = self._config.model_context_window
276
274
  if context_window is None and model_metadata is not None:
@@ -293,7 +291,7 @@ class ContextManager:
293
291
  model_slug = self._config.model
294
292
  if model_slug is None:
295
293
  return None
296
- model_metadata = _load_models_by_slug().get(model_slug)
294
+ model_metadata = load_models_by_slug().get(model_slug)
297
295
  if model_metadata is None:
298
296
  return None
299
297
  return _normalize_int(model_metadata.get("auto_compact_token_limit"))
@@ -302,7 +300,7 @@ class ContextManager:
302
300
  model_slug = self._config.model
303
301
  if model_slug is None:
304
302
  return None
305
- model_metadata = _load_models_by_slug().get(model_slug)
303
+ model_metadata = load_models_by_slug().get(model_slug)
306
304
  if model_metadata is None:
307
305
  return None
308
306
 
@@ -560,18 +558,6 @@ def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
560
558
  return None
561
559
 
562
560
 
563
- @lru_cache(maxsize=1)
564
- def _load_models_by_slug() -> 'typing.Dict[str, JSONDict]':
565
- payload = json.loads(DEFAULT_MODELS_PATH.read_text(encoding="utf-8"))
566
- models = payload.get("models", [])
567
- by_slug: 'typing.Dict[str, JSONDict]' = {}
568
- for model in models:
569
- slug = model.get("slug")
570
- if isinstance(slug, str):
571
- by_slug[slug] = model
572
- return by_slug
573
-
574
-
575
561
  def _resolve_personality_message(variables, personality: 'typing.Union[str, None]') -> 'str':
576
562
  if not isinstance(variables, dict):
577
563
  return ""
@@ -25,10 +25,11 @@ MODEL_COMMAND = "/model"
25
25
  QUEUE_COMMAND = "/queue"
26
26
  RESUME_COMMAND = "/resume"
27
27
  COMPACT_COMMAND = "/compact"
28
+ FORK_COMMAND = "/fork"
28
29
  LINK_COMMAND = "/link"
29
30
  UNLINK_COMMAND = "/unlink"
30
31
  EXTRA_COMMANDS_LINE = (
31
- "Extra commands: /help, /history, /title, /model, /resume, /compact, /link, /unlink"
32
+ "Extra commands: /help, /history, /title, /model, /resume, /compact, /fork, /link, /unlink"
32
33
  )
33
34
 
34
35
 
@@ -301,6 +302,19 @@ async def run_interactive_session(
301
302
  except Exception as exc: # pragma: no cover - defensive surface
302
303
  view.show_error(str(exc))
303
304
  continue
305
+ if prompt_text == FORK_COMMAND:
306
+ if has_pending_turn_tasks():
307
+ view.write_line(
308
+ "Cannot fork while work is running or queued."
309
+ )
310
+ continue
311
+ if not hasattr(model_client, "_session_id"):
312
+ view.write_line("Current model does not support session IDs.")
313
+ continue
314
+ new_session_id = uuid7_string()
315
+ model_client._session_id = new_session_id
316
+ view.write_line(f"Forked session: {new_session_id}")
317
+ continue
304
318
  if prompt_text.startswith(f"{LINK_COMMAND} "):
305
319
  link_target = prompt_text[len(LINK_COMMAND) :].strip()
306
320
  if not link_target:
@@ -19,16 +19,19 @@ except ModuleNotFoundError: # pragma: no cover - Python 3.10 path
19
19
 
20
20
  from .protocol import (
21
21
  AssistantMessage,
22
+ JSONDict,
22
23
  ModelResponse,
23
24
  ModelStreamEvent,
24
25
  Prompt,
25
26
  ReasoningItem,
26
27
  ToolCall,
27
28
  )
29
+ from .model_metadata import model_metadata
28
30
  from .utils import build_user_agent, uuid7_string
29
31
 
30
32
  DEFAULT_CODEX_CONFIG_PATH = Path.home() / ".codex" / "config.toml"
31
33
  DEFAULT_ORIGINATOR = "pycodex"
34
+ RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
32
35
  ModelStreamEventHandler = Callable[[ModelStreamEvent], None]
33
36
  NOOP_MODEL_STREAM_EVENT_HANDLER: 'ModelStreamEventHandler' = lambda _event: None
34
37
  DEFAULT_STREAM_MAX_RETRIES = 5
@@ -65,6 +68,7 @@ class ResponsesProviderConfig:
65
68
  beta_features_header: 'typing.Union[str, None]' = None
66
69
  stream_max_retries: 'typing.Union[int, None]' = None
67
70
  stream_idle_timeout_ms: 'typing.Union[int, None]' = None
71
+ service_tier: 'typing.Union[str, None]' = None
68
72
 
69
73
  @classmethod
70
74
  def from_codex_config(
@@ -116,6 +120,7 @@ class ResponsesProviderConfig:
116
120
  reasoning_effort=selected.get("model_reasoning_effort"),
117
121
  reasoning_summary=selected.get("model_reasoning_summary"),
118
122
  verbosity=selected.get("model_verbosity"),
123
+ service_tier=selected.get("service_tier"),
119
124
  sandbox_mode=selected.get("sandbox_mode"),
120
125
  beta_features_header=",".join(beta_features) or None,
121
126
  stream_max_retries=_optional_int(provider.get("stream_max_retries")),
@@ -157,6 +162,65 @@ class ResponsesProviderConfig:
157
162
  return DEFAULT_STREAM_IDLE_TIMEOUT_MS / 1000.0
158
163
  return max(int(self.stream_idle_timeout_ms), 1) / 1000.0
159
164
 
165
+ def metadata(self) -> 'typing.Union[JSONDict, None]':
166
+ return model_metadata(self.model)
167
+
168
+ def use_responses_lite(self) -> 'bool':
169
+ metadata = self.metadata()
170
+ if metadata is None:
171
+ return False
172
+ return metadata.get("use_responses_lite") is True
173
+
174
+ def effective_reasoning_effort(self) -> 'typing.Union[str, None]':
175
+ if self.reasoning_effort is not None:
176
+ return str(self.reasoning_effort)
177
+ metadata = self.metadata()
178
+ if not _metadata_supports_reasoning(metadata):
179
+ return None
180
+ return _optional_metadata_string(metadata, "default_reasoning_level")
181
+
182
+ def effective_reasoning_summary(self) -> 'typing.Union[str, None]':
183
+ summary = self.reasoning_summary
184
+ if summary is None:
185
+ metadata = self.metadata()
186
+ if not _metadata_supports_reasoning(metadata):
187
+ return None
188
+ summary = _optional_metadata_string(metadata, "default_reasoning_summary")
189
+ if summary is None:
190
+ return None
191
+ if str(summary).strip().lower() == "none":
192
+ return None
193
+ return str(summary)
194
+
195
+ def effective_verbosity(self) -> 'typing.Union[str, None]':
196
+ if self.verbosity is not None:
197
+ return str(self.verbosity)
198
+ metadata = self.metadata()
199
+ if metadata is None or metadata.get("support_verbosity") is not True:
200
+ return None
201
+ return _optional_metadata_string(metadata, "default_verbosity")
202
+
203
+ def effective_service_tier(self) -> 'typing.Union[str, None]':
204
+ service_tier = self.service_tier
205
+ if service_tier is None:
206
+ return None
207
+ service_tier = str(service_tier).strip()
208
+ if service_tier == "fast":
209
+ service_tier = "priority"
210
+ if not service_tier or service_tier == "default":
211
+ return None
212
+
213
+ metadata = self.metadata()
214
+ if metadata is None:
215
+ return None
216
+ supported = metadata.get("service_tiers")
217
+ if not isinstance(supported, list):
218
+ return None
219
+ for tier in supported:
220
+ if isinstance(tier, dict) and tier.get("id") == service_tier:
221
+ return service_tier
222
+ return None
223
+
160
224
 
161
225
  def _optional_bool(value: 'typing.Union[bool, str, int, None]') -> 'typing.Union[bool, None]':
162
226
  if value is None:
@@ -171,6 +235,43 @@ def _optional_bool(value: 'typing.Union[bool, str, int, None]') -> 'typing.Union
171
235
  raise ValueError(f"invalid boolean config value: {value!r}")
172
236
 
173
237
 
238
+ def _metadata_supports_reasoning(
239
+ metadata: 'typing.Union[JSONDict, None]',
240
+ ) -> 'bool':
241
+ return metadata is not None and metadata.get("supports_reasoning_summaries") is True
242
+
243
+
244
+ def _optional_metadata_string(
245
+ metadata: 'typing.Union[JSONDict, None]',
246
+ key: 'str',
247
+ ) -> 'typing.Union[str, None]':
248
+ if metadata is None:
249
+ return None
250
+ value = metadata.get(key)
251
+ if value is None:
252
+ return None
253
+ text = str(value).strip()
254
+ return text or None
255
+
256
+
257
+ def _strip_image_details(items: 'typing.Iterable[object]') -> 'None':
258
+ for item in items:
259
+ if not isinstance(item, dict):
260
+ continue
261
+ content = item.get("content")
262
+ if isinstance(content, list):
263
+ _strip_image_detail_from_content_items(content)
264
+ output = item.get("output")
265
+ if isinstance(output, list):
266
+ _strip_image_detail_from_content_items(output)
267
+
268
+
269
+ def _strip_image_detail_from_content_items(items: 'typing.Iterable[object]') -> 'None':
270
+ for item in items:
271
+ if isinstance(item, dict) and item.get("type") == "input_image":
272
+ item.pop("detail", None)
273
+
274
+
174
275
  class ResponsesApiError(RuntimeError):
175
276
  pass
176
277
 
@@ -392,34 +493,73 @@ class ResponsesModelClient:
392
493
  ) from exc
393
494
 
394
495
  def _build_payload(self, prompt: 'Prompt') -> 'typing.Dict[str, object]':
496
+ use_responses_lite = self._config.use_responses_lite()
497
+ input_items = [item.serialize() for item in prompt.input]
498
+ if use_responses_lite:
499
+ _strip_image_details(input_items)
500
+
501
+ tools = [tool.serialize() for tool in prompt.tools]
395
502
  payload: 'typing.Dict[str, object]' = {
396
503
  "model": self.model,
397
- "instructions": prompt.base_instructions or "",
398
- "input": [item.serialize() for item in prompt.input],
399
- "tools": [tool.serialize() for tool in prompt.tools],
400
- "parallel_tool_calls": prompt.parallel_tool_calls,
504
+ "input": input_items,
505
+ "parallel_tool_calls": prompt.parallel_tool_calls and not use_responses_lite,
401
506
  "store": False,
402
507
  "stream": True,
403
508
  "include": ["reasoning.encrypted_content"],
404
509
  "prompt_cache_key": self._session_id,
405
510
  }
406
- if prompt.tools:
511
+ if use_responses_lite:
512
+ prefix: 'typing.List[typing.Dict[str, object]]' = [
513
+ {
514
+ "type": "additional_tools",
515
+ "role": "developer",
516
+ "tools": tools,
517
+ }
518
+ ]
519
+ if prompt.base_instructions:
520
+ prefix.append(
521
+ {
522
+ "type": "message",
523
+ "role": "developer",
524
+ "content": [
525
+ {
526
+ "type": "input_text",
527
+ "text": prompt.base_instructions,
528
+ }
529
+ ],
530
+ }
531
+ )
532
+ payload["input"] = prefix + input_items
533
+ else:
534
+ payload["instructions"] = prompt.base_instructions or ""
535
+ payload["tools"] = tools
536
+
537
+ if prompt.tools or use_responses_lite:
407
538
  payload["tool_choice"] = "auto"
408
539
 
409
540
  reasoning: 'typing.Dict[str, str]' = {}
410
- if self._config.reasoning_effort is not None:
411
- reasoning["effort"] = self._config.reasoning_effort
412
- if self._config.reasoning_summary is not None:
413
- reasoning["summary"] = self._config.reasoning_summary
541
+ reasoning_effort = self._config.effective_reasoning_effort()
542
+ reasoning_summary = self._config.effective_reasoning_summary()
543
+ if reasoning_effort is not None:
544
+ reasoning["effort"] = reasoning_effort
545
+ if reasoning_summary is not None:
546
+ reasoning["summary"] = reasoning_summary
547
+ if use_responses_lite and reasoning:
548
+ reasoning["context"] = "all_turns"
414
549
  if reasoning:
415
550
  payload["reasoning"] = reasoning
416
551
 
417
552
  text = None
418
- if self._config.verbosity is not None:
419
- text = {"verbosity": self._config.verbosity}
553
+ verbosity = self._config.effective_verbosity()
554
+ if verbosity is not None:
555
+ text = {"verbosity": verbosity}
420
556
  if text is not None:
421
557
  payload["text"] = text
422
558
 
559
+ service_tier = self._config.effective_service_tier()
560
+ if service_tier is not None:
561
+ payload["service_tier"] = service_tier
562
+
423
563
  return payload
424
564
 
425
565
  def _list_models_sync(self) -> 'typing.List[str]':
@@ -483,6 +623,8 @@ class ResponsesModelClient:
483
623
  headers["authorization"] = f"Bearer {api_key}"
484
624
  if self._config.beta_features_header is not None:
485
625
  headers["x-codex-beta-features"] = self._config.beta_features_header
626
+ if self._config.use_responses_lite():
627
+ headers[RESPONSES_LITE_HEADER] = "true"
486
628
  if self._openai_subagent is not None:
487
629
  headers["x-openai-subagent"] = self._openai_subagent
488
630
  if prompt.turn_metadata is not None:
@@ -0,0 +1,31 @@
1
+ """Shared loader for vendored Codex model metadata."""
2
+
3
+ from functools import lru_cache
4
+ import json
5
+ from pathlib import Path
6
+ import typing
7
+
8
+ from .protocol import JSONDict
9
+
10
+
11
+ DEFAULT_MODELS_PATH = Path(__file__).resolve().parent / "prompts" / "models.json"
12
+
13
+
14
+ @lru_cache(maxsize=1)
15
+ def load_models_by_slug() -> 'typing.Dict[str, JSONDict]':
16
+ payload = json.loads(DEFAULT_MODELS_PATH.read_text(encoding="utf-8"))
17
+ models = payload.get("models", [])
18
+ by_slug: 'typing.Dict[str, JSONDict]' = {}
19
+ for model in models:
20
+ if not isinstance(model, dict):
21
+ continue
22
+ slug = model.get("slug")
23
+ if isinstance(slug, str):
24
+ by_slug[slug] = model
25
+ return by_slug
26
+
27
+
28
+ def model_metadata(slug: 'typing.Union[str, None]') -> 'typing.Union[JSONDict, None]':
29
+ if slug is None:
30
+ return None
31
+ return load_models_by_slug().get(slug)