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.
- {python_codex-0.2.4 → python_codex-0.2.5}/AGENTS.md +2 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/PKG-INFO +16 -4
- {python_codex-0.2.4 → python_codex-0.2.5}/README.md +15 -3
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/context.py +4 -18
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/interactive_session.py +15 -1
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/model.py +153 -11
- python_codex-0.2.5/pycodex/model_metadata.py +31 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/models.json +334 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/unified_exec_manager.py +32 -1
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/compactor.py +5 -87
- {python_codex-0.2.4 → python_codex-0.2.5}/pyproject.toml +1 -1
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_agent.py +7 -14
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_builtin_tools.py +58 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_cli.py +32 -5
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_compactor.py +9 -9
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_context.py +10 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_model.py +186 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_workspace_server.py +155 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/app.py +57 -1
- {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/workspace.html +44 -1
- {python_codex-0.2.4 → python_codex-0.2.5}/.github/workflows/publish.yml +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/.github/workflows/test.yml +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/.gitignore +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/LICENSE +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/README_ZH.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/docs/ALIGNMENT.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/docs/CONTEXT.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/docs/responses_server/README.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/__init__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/agent.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/cli.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/collaboration.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/compat.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/doctor.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/feishu_card.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/feishu_link.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/portable.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/portable_server.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/collaboration_default.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/collaboration_plan.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/default_base_instructions.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/protocol.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/runtime.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/runtime_services.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/__init__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/agent_tool_schemas.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/apply_patch_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/base_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/close_agent_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/code_mode_manager.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/exec_command_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/exec_runtime.js +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/exec_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/grep_files_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/ipython_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/list_dir_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/read_file_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/request_permissions_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/request_user_input_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/resume_agent_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/send_input_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/shell_command_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/shell_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/spawn_agent_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/update_plan_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/view_image_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/wait_agent_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/wait_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/web_search_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/tools/write_stdin_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/__init__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/async_bridge.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/debug.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/dotenv.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/get_env.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/random_ids.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/session_persist.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/toolcall_visualize.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/truncation.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/pycodex/utils/visualize.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/__init__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/__main__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/app.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/config.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/messages_api.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/payload_processors.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/server.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/session_store.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/stream_router.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/tools/__init__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/tools/custom_adapter.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/tools/web_search.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/responses_server/trajectory_dump.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/TESTS.md +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/__init__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/compare_request_user_input_roundtrip.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/compare_steer_request_bodies.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/compare_tool_schemas.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/fake_responses_server.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/fakes.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/responses_server/fake_chat_completions_server.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/responses_server/test_server.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_async_bridge.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_doctor.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_fake_responses_server.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_feishu_card.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_feishu_link.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_ipython_tool.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_portable.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_py36_syntax.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tests/test_visualize.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/tools/feishu_oauth.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/__init__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/__main__.py +0 -0
- {python_codex-0.2.4 → python_codex-0.2.5}/workspace_server/workspaces.html +0 -0
- {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.
|
|
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`,
|
|
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.
|
|
223
|
-
|
|
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`,
|
|
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.
|
|
202
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
"
|
|
398
|
-
"
|
|
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
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
if
|
|
413
|
-
reasoning["
|
|
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
|
-
|
|
419
|
-
|
|
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)
|