python-codex 0.1.5__tar.gz → 0.1.7__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 (103) hide show
  1. {python_codex-0.1.5 → python_codex-0.1.7}/AGENTS.md +2 -0
  2. {python_codex-0.1.5 → python_codex-0.1.7}/PKG-INFO +5 -1
  3. {python_codex-0.1.5 → python_codex-0.1.7}/README.md +4 -0
  4. {python_codex-0.1.5 → python_codex-0.1.7}/docs/responses_server/README.md +21 -2
  5. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/cli.py +59 -6
  6. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/base_tool.py +16 -0
  7. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/utils/__init__.py +2 -0
  8. python_codex-0.1.7/pycodex/utils/debug.py +12 -0
  9. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/utils/visualize.py +34 -7
  10. {python_codex-0.1.5 → python_codex-0.1.7}/pyproject.toml +1 -1
  11. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/app.py +10 -1
  12. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/config.py +12 -0
  13. python_codex-0.1.7/responses_server/messages_api.py +479 -0
  14. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/payload_processors.py +1 -0
  15. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/stream_router.py +94 -0
  16. {python_codex-0.1.5 → python_codex-0.1.7}/tests/responses_server/fake_chat_completions_server.py +196 -0
  17. {python_codex-0.1.5 → python_codex-0.1.7}/tests/responses_server/test_server.py +277 -0
  18. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_cli.py +130 -2
  19. {python_codex-0.1.5 → python_codex-0.1.7}/.github/workflows/publish.yml +0 -0
  20. {python_codex-0.1.5 → python_codex-0.1.7}/.github/workflows/test.yml +0 -0
  21. {python_codex-0.1.5 → python_codex-0.1.7}/.gitignore +0 -0
  22. {python_codex-0.1.5 → python_codex-0.1.7}/LICENSE +0 -0
  23. {python_codex-0.1.5 → python_codex-0.1.7}/README_ZH.md +0 -0
  24. {python_codex-0.1.5 → python_codex-0.1.7}/docs/ALIGNMENT.md +0 -0
  25. {python_codex-0.1.5 → python_codex-0.1.7}/docs/CONTEXT.md +0 -0
  26. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/__init__.py +0 -0
  27. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/agent.py +0 -0
  28. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/collaboration.py +0 -0
  29. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/compat.py +0 -0
  30. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/context.py +0 -0
  31. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/doctor.py +0 -0
  32. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/model.py +0 -0
  33. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/portable.py +0 -0
  34. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/portable_server.py +0 -0
  35. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/collaboration_default.md +0 -0
  36. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/collaboration_plan.md +0 -0
  37. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/default_base_instructions.md +0 -0
  38. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/exec_tools.json +0 -0
  39. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/models.json +0 -0
  40. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
  41. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
  42. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
  43. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
  44. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
  45. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
  46. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
  47. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
  48. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/prompts/subagent_tools.json +0 -0
  49. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/protocol.py +0 -0
  50. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/runtime.py +0 -0
  51. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/runtime_services.py +0 -0
  52. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/__init__.py +0 -0
  53. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/agent_tool_schemas.py +0 -0
  54. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/apply_patch_tool.py +0 -0
  55. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/close_agent_tool.py +0 -0
  56. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/code_mode_manager.py +0 -0
  57. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/exec_command_tool.py +0 -0
  58. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/exec_runtime.js +0 -0
  59. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/exec_tool.py +0 -0
  60. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/grep_files_tool.py +0 -0
  61. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/list_dir_tool.py +0 -0
  62. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/read_file_tool.py +0 -0
  63. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/request_permissions_tool.py +0 -0
  64. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/request_user_input_tool.py +0 -0
  65. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/resume_agent_tool.py +0 -0
  66. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/send_input_tool.py +0 -0
  67. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/shell_command_tool.py +0 -0
  68. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/shell_tool.py +0 -0
  69. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/spawn_agent_tool.py +0 -0
  70. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/unified_exec_manager.py +0 -0
  71. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/update_plan_tool.py +0 -0
  72. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/view_image_tool.py +0 -0
  73. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/wait_agent_tool.py +0 -0
  74. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/wait_tool.py +0 -0
  75. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/web_search_tool.py +0 -0
  76. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/tools/write_stdin_tool.py +0 -0
  77. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/utils/compactor.py +0 -0
  78. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/utils/dotenv.py +0 -0
  79. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/utils/get_env.py +0 -0
  80. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/utils/random_ids.py +0 -0
  81. {python_codex-0.1.5 → python_codex-0.1.7}/pycodex/utils/session_persist.py +0 -0
  82. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/__init__.py +0 -0
  83. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/__main__.py +0 -0
  84. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/server.py +0 -0
  85. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/session_store.py +0 -0
  86. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/tools/__init__.py +0 -0
  87. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/tools/custom_adapter.py +0 -0
  88. {python_codex-0.1.5 → python_codex-0.1.7}/responses_server/tools/web_search.py +0 -0
  89. {python_codex-0.1.5 → python_codex-0.1.7}/tests/TESTS.md +0 -0
  90. {python_codex-0.1.5 → python_codex-0.1.7}/tests/__init__.py +0 -0
  91. {python_codex-0.1.5 → python_codex-0.1.7}/tests/compare_request_user_input_roundtrip.py +0 -0
  92. {python_codex-0.1.5 → python_codex-0.1.7}/tests/compare_steer_request_bodies.py +0 -0
  93. {python_codex-0.1.5 → python_codex-0.1.7}/tests/compare_tool_schemas.py +0 -0
  94. {python_codex-0.1.5 → python_codex-0.1.7}/tests/fake_responses_server.py +0 -0
  95. {python_codex-0.1.5 → python_codex-0.1.7}/tests/fakes.py +0 -0
  96. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_agent.py +0 -0
  97. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_builtin_tools.py +0 -0
  98. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_compactor.py +0 -0
  99. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_context.py +0 -0
  100. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_doctor.py +0 -0
  101. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_fake_responses_server.py +0 -0
  102. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_model.py +0 -0
  103. {python_codex-0.1.5 → python_codex-0.1.7}/tests/test_portable.py +0 -0
@@ -13,6 +13,8 @@
13
13
  - `responses_server` compat 层应透传请求里的 `model`;不要再做 “取 downstream /models 第一个 id 并强制覆盖请求模型” 这种兜底兼容。
14
14
  - 对 `model_provider = "vllm"`,`responses_server` 仍然走 `/v1/chat/completions` compat 路径,但要保留 reasoning:把 chat chunk 里的 `reasoning` / `reasoning_content` 翻回 Responses `reasoning` item,并把历史里的 Responses `reasoning` item 回放成下游 assistant message 的 `reasoning` 字段。
15
15
  - `responses_server` 的 provider-specific chat payload 定制统一放在 `responses_server/payload_processors.py`:使用 `CompatServerConfig.model_provider` 选择 `provider_name -> proc_fn(outcomming_request)` 映射,并且只在真正发出 downstream `/v1/chat/completions` 前 post-process;`StreamRouter` 内部继续保留 canonical payload,避免 tool hydration / mock web_search follow-up 被 provider 改写污染。
16
+ - `responses_server` 如果要兼容下游 `/v1/messages`,也优先保持这条边界:内部继续用 canonical chat request / chat-like chunk 流,只有真正发请求和读取 SSE 时才做 messages 适配,这样 tool hydration、mock `web_search` follow-up、provider payload post-process 都能复用。
17
+ - 真实 vLLM `0.19.0` 的 `/v1/messages` 会对缺失 `max_tokens` 直接返回 `400`;messages 适配层必须总是补这个字段。当前约定是优先透传请求里的 `max_output_tokens`/`max_tokens`,否则回退到默认 `32000`。
16
18
  - `pycodex` 默认是最小交互 CLI;无 prompt 时进入 REPL,并通过 `AgentRuntime` 跑外层提交循环。当前会显示最小事件流、assistant 流式输出、简单 title/history(`/title`, `/history`),并默认注册一组与原版一一对应的本地工具子集。
17
19
  - 交互 CLI 的事件流展示优先表达用户可感知的阶段(例如工具开始/完成、模型回看工具结果),不要直接把内部 `iteration` 计数暴露成主要状态文案;`iterations` 应继续保留在 `TurnResult` 等程序化结果里。
18
20
  - prompt/context 相关逻辑统一放在 `pycodex/context.py`:`AgentLoop` 只维护真实会话历史;每轮请求前由 `ContextManager` 注入 base instructions、developer message、`AGENTS.md` 指令和 `<environment_context>`,且这些注入项不写回 history。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -159,6 +159,7 @@ pycodex "Summarize this repo in one sentence."
159
159
  printf 'Reply with exactly OK.' | pycodex
160
160
  pycodex --json "Reply with exactly OK."
161
161
  pycodex --profile model_proxy "Reply with exactly OK."
162
+ pycodex --profile opus --use-messages "Reply with exactly OK."
162
163
  pycodex --vllm-endpoint http://127.0.0.1:18000 "Reply with exactly OK."
163
164
  pycodex --put @127.0.0.1:5577
164
165
  pycodex --put /data/.codex/@127.0.0.1:5577
@@ -211,6 +212,9 @@ Current behavior:
211
212
  historical `reasoning` items are replayed into downstream assistant messages
212
213
  via the `reasoning` field. Streaming token usage is also requested from vLLM
213
214
  and forwarded to the final `response.completed.response.usage`
215
+ - standalone `responses_server` now also supports downstream `/v1/messages`
216
+ backends via `--outcomming-api messages`, while keeping the internal
217
+ canonical request/route logic in chat-completions shape
214
218
  - `pycodex doctor` checks config, `.env`, API keys, DNS, TCP/TLS, and an
215
219
  optional live Responses API request
216
220
 
@@ -138,6 +138,7 @@ pycodex "Summarize this repo in one sentence."
138
138
  printf 'Reply with exactly OK.' | pycodex
139
139
  pycodex --json "Reply with exactly OK."
140
140
  pycodex --profile model_proxy "Reply with exactly OK."
141
+ pycodex --profile opus --use-messages "Reply with exactly OK."
141
142
  pycodex --vllm-endpoint http://127.0.0.1:18000 "Reply with exactly OK."
142
143
  pycodex --put @127.0.0.1:5577
143
144
  pycodex --put /data/.codex/@127.0.0.1:5577
@@ -190,6 +191,9 @@ Current behavior:
190
191
  historical `reasoning` items are replayed into downstream assistant messages
191
192
  via the `reasoning` field. Streaming token usage is also requested from vLLM
192
193
  and forwarded to the final `response.completed.response.usage`
194
+ - standalone `responses_server` now also supports downstream `/v1/messages`
195
+ backends via `--outcomming-api messages`, while keeping the internal
196
+ canonical request/route logic in chat-completions shape
193
197
  - `pycodex doctor` checks config, `.env`, API keys, DNS, TCP/TLS, and an
194
198
  optional live Responses API request
195
199
 
@@ -21,6 +21,7 @@
21
21
  - incomming `POST /v1/responses`
22
22
  - incomming `GET /v1/models`
23
23
  - outcomming `POST /v1/chat/completions`
24
+ - outcomming `POST /v1/messages`(通过边界适配复用同一套 canonical chat request / stream routing)
24
25
  - 流式 assistant 文本
25
26
  - vLLM chat-completions `reasoning` / `reasoning_content` -> Responses `reasoning` item 适配
26
27
  - vLLM 历史 `reasoning` item -> assistant message `reasoning` 字段回放
@@ -42,13 +43,13 @@
42
43
  ## Incomming / Outcomming 分层
43
44
 
44
45
  - incomming:面向 Codex 的 Responses 子集
45
- - outcomming:面向 backend 的 chat completions 子集
46
+ - outcomming:面向 backend 的 chat-completions / messages 兼容子集
46
47
 
47
48
  当前职责拆分:
48
49
 
49
50
  - `responses_server/app.py`:FastAPI app 和 CLI 入口
50
51
  - `responses_server/server.py`:`ResponseServer`,负责持有 `SessionStore` 和 `StreamRouter`
51
- - `responses_server/stream_router.py`:`StreamRouter`,负责 incomming 请求翻译、outcomming chat 请求和流路由;对 `model_provider = "vllm"` 额外适配 chat-level reasoning
52
+ - `responses_server/stream_router.py`:`StreamRouter`,负责 incomming 请求翻译、outcomming request 和流路由;对 `model_provider = "vllm"` 额外适配 chat-level reasoning
52
53
  - `responses_server/payload_processors.py`:按 `CompatServerConfig.model_provider` 选择 provider-specific payload `post_process`
53
54
  - `responses_server/tools/`:provider 侧工具适配层;当前放 mock `web_search` 和 custom-tool function wrapper
54
55
  - `responses_server/session_store.py`:最小隐藏状态存储
@@ -63,6 +64,15 @@ uv run python -m responses_server \
63
64
  --model-provider vllm
64
65
  ```
65
66
 
67
+ 如果下游不是 `/v1/chat/completions`,而是 Anthropic/Claude 风格的
68
+ `/v1/messages`,再额外加:
69
+
70
+ ```bash
71
+ uv run python -m responses_server \
72
+ --outcomming-base-url http://127.0.0.1:8000/v1 \
73
+ --outcomming-api messages
74
+ ```
75
+
66
76
  默认会在本地启动一个 incomming Responses 服务;真正监听地址由 `--host` 和 `--port`
67
77
  控制。
68
78
 
@@ -73,6 +83,15 @@ uv run python -m responses_server \
73
83
  当前内置规则里,`vllm` 仍走 chat-completions compat 路径,但会额外保留
74
84
  reasoning;`stepfun` 会删除所有 `developer` role。
75
85
 
86
+ `messages` compat 则故意不改这层 canonical request:仍然先构造 chat 风格
87
+ `outcomming_request`,只有在真正发请求和读 SSE 时,才在边界把它翻译成
88
+ messages request / event。这样 tool hydration、mock `web_search`
89
+ follow-up、provider payload post-process 仍然复用同一套主逻辑。
90
+
91
+ 当前 messages 边界还会补一个兼容性细节:下游如果像 vLLM `0.19.0` 一样要求
92
+ `max_tokens`,则优先透传上游请求里的 `max_output_tokens` / `max_tokens`;
93
+ 如果上游没给,当前默认补 `32000`,避免直接被下游 `400` 拒绝。
94
+
76
95
  ## 验证
77
96
 
78
97
  当前独立测试:
@@ -7,6 +7,7 @@ import os
7
7
  import shlex
8
8
  import sys
9
9
  import tempfile
10
+ import traceback
10
11
  from dataclasses import asdict, replace
11
12
  from pathlib import Path
12
13
  from typing import Sequence
@@ -20,7 +21,7 @@ from .portable import bootstrap_called_home, upload_codex_home
20
21
  from .protocol import AgentEvent
21
22
  from .runtime import AgentRuntime
22
23
  from .runtime_services import RuntimeEnvironment, create_runtime_environment
23
- from .utils import CliSessionView, load_codex_dotenv, uuid7_string
24
+ from .utils import CliSessionView, get_debug_dir, load_codex_dotenv, uuid7_string
24
25
  from .utils.compactor import compact_agent_loop
25
26
  from .utils.session_persist import (
26
27
  SessionRolloutRecorder,
@@ -42,7 +43,6 @@ CliSessionMode = Literal["exec", "tui"]
42
43
  LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
43
44
  CLI_ORIGINATOR = "codex-tui"
44
45
 
45
-
46
46
  def launch_chat_completion_compat_server(*args, **kwargs):
47
47
  from responses_server import (
48
48
  launch_chat_completion_compat_server as launch_compat_server,
@@ -58,9 +58,9 @@ def configure_loguru() -> 'None':
58
58
  return
59
59
 
60
60
  logger.remove()
61
- log_path = os.environ.get("PYCODEX_DEBUG_LOG", "").strip()
62
- if log_path:
63
- logger.add(log_path, level="DEBUG")
61
+ debug_dir = get_debug_dir()
62
+ if debug_dir is not None:
63
+ logger.add(str(debug_dir / "loguru.log"), level="DEBUG")
64
64
  return
65
65
 
66
66
  if os.environ.get("PYCODEX_DEBUG_STDERR", "").strip().lower() in {
@@ -123,6 +123,15 @@ def build_parser() -> 'argparse.ArgumentParser':
123
123
  "When set, pycodex starts a local responses compat server for this session."
124
124
  ),
125
125
  )
126
+ parser.add_argument(
127
+ "--use-messages",
128
+ default=False,
129
+ action="store_true",
130
+ help=(
131
+ "When set, pycodex starts a local responses compat server and routes "
132
+ "to a downstream /v1/messages backend for this session."
133
+ ),
134
+ )
126
135
  parser.add_argument(
127
136
  "--system-prompt",
128
137
  default=None,
@@ -373,12 +382,17 @@ def _build_model_client(
373
382
  managed_responses_base_url: 'typing.Union[str, None]' = None,
374
383
  vllm_endpoint: 'typing.Union[str, None]' = None,
375
384
  use_chat_completion: 'bool' = False,
385
+ use_messages: 'bool' = False,
376
386
  ):
377
387
  load_codex_dotenv(config_path)
378
388
  provider_config = ResponsesProviderConfig.from_codex_config(
379
389
  config_path,
380
390
  profile,
381
391
  )
392
+ if use_chat_completion and use_messages:
393
+ raise ValueError("--use-chat-completion and --use-messages cannot be combined")
394
+ if vllm_endpoint and use_messages:
395
+ raise ValueError("--vllm-endpoint and --use-messages cannot be combined")
382
396
  url, key_env = provider_config.base_url, provider_config.api_key_env
383
397
  if managed_responses_base_url is not None:
384
398
  url, key_env = (
@@ -386,7 +400,7 @@ def _build_model_client(
386
400
  LOCAL_RESPONSES_SERVER_API_KEY_ENV,
387
401
  )
388
402
  os.environ.setdefault(LOCAL_RESPONSES_SERVER_API_KEY_ENV, "dummy")
389
- elif vllm_endpoint or use_chat_completion:
403
+ elif vllm_endpoint or use_chat_completion or use_messages:
390
404
  if vllm_endpoint:
391
405
  managed_server = launch_chat_completion_compat_server(
392
406
  vllm_endpoint,
@@ -397,6 +411,9 @@ def _build_model_client(
397
411
  provider_config.base_url,
398
412
  provider_config.api_key_env,
399
413
  model_provider=provider_config.provider_name,
414
+ outcomming_api=(
415
+ "messages" if use_messages else "chat_completions"
416
+ ),
400
417
  )
401
418
  atexit.register(managed_server.stop)
402
419
  url, key_env = (
@@ -727,6 +744,8 @@ async def run_interactive_session(
727
744
  async def run_cli(args: 'argparse.Namespace') -> 'int':
728
745
  runtime = None
729
746
  worker = None
747
+ debug_dir = get_debug_dir()
748
+ phase_handle = None if debug_dir is None else (debug_dir / "phase.log").open("a", encoding="utf-8")
730
749
  try:
731
750
  if args.put is not None and args.call:
732
751
  raise ValueError("--put and --call cannot be combined")
@@ -746,17 +765,33 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
746
765
  print(f"pycodex --call {shlex.quote(call_spec)}", flush=True)
747
766
  return 0
748
767
  if args.call:
768
+ if phase_handle is not None:
769
+ phase_handle.write("bootstrap_called_home:start\n")
770
+ phase_handle.flush()
749
771
  config_path = bootstrap_called_home(args.call)
772
+ if phase_handle is not None:
773
+ phase_handle.write("bootstrap_called_home:done\n")
774
+ phase_handle.flush()
750
775
  args.config = str(config_path)
751
776
  os.environ["CODEX_HOME"] = str(config_path.parent)
777
+ if phase_handle is not None:
778
+ phase_handle.write("build_model_client:start\n")
779
+ phase_handle.flush()
752
780
  client = _build_model_client(
753
781
  args.config,
754
782
  args.profile,
755
783
  args.timeout_seconds,
756
784
  vllm_endpoint=args.vllm_endpoint,
757
785
  use_chat_completion=args.use_chat_completion,
786
+ use_messages=args.use_messages,
758
787
  )
788
+ if phase_handle is not None:
789
+ phase_handle.write("build_model_client:done\n")
790
+ phase_handle.flush()
759
791
 
792
+ if phase_handle is not None:
793
+ phase_handle.write("build_runtime:start\n")
794
+ phase_handle.flush()
760
795
  runtime = build_runtime(
761
796
  args.config,
762
797
  args.profile,
@@ -764,6 +799,9 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
764
799
  client,
765
800
  session_mode="tui",
766
801
  )
802
+ if phase_handle is not None:
803
+ phase_handle.write("build_runtime:done\n")
804
+ phase_handle.flush()
767
805
  if should_run_interactive(args.prompt, sys.stdin.isatty()):
768
806
  return await run_interactive_session(
769
807
  runtime,
@@ -773,13 +811,28 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
773
811
  else:
774
812
  prompt_text = resolve_prompt_text(args.prompt)
775
813
  worker = asyncio.create_task(runtime.run_forever())
814
+ if phase_handle is not None:
815
+ phase_handle.write("submit_user_turn:start\n")
816
+ phase_handle.flush()
776
817
  result = await runtime.submit_user_turn(prompt_text)
818
+ if phase_handle is not None:
819
+ phase_handle.write("submit_user_turn:done\n")
820
+ phase_handle.flush()
777
821
  print(format_turn_output(result, args.json))
778
822
  return 0
779
823
  except Exception as exc:
824
+ if phase_handle is not None:
825
+ phase_handle.write("fatal_exception\n")
826
+ phase_handle.flush()
827
+ if debug_dir is not None:
828
+ (debug_dir / "fatal_error.txt").write_text(
829
+ traceback.format_exc(), encoding="utf-8"
830
+ )
780
831
  print(f"Error: {exc}", file=sys.stderr)
781
832
  return 1
782
833
  finally:
834
+ if phase_handle is not None:
835
+ phase_handle.close()
783
836
  if runtime is not None and worker is not None:
784
837
  await runtime.shutdown()
785
838
  await worker
@@ -16,8 +16,10 @@ from dataclasses import dataclass
16
16
  from functools import lru_cache
17
17
  import json
18
18
  from pathlib import Path
19
+ import traceback
19
20
 
20
21
  from ..protocol import ConversationItem, JSONDict, JSONValue, ToolCall, ToolResult, ToolSpec
22
+ from ..utils import get_debug_dir
21
23
  import typing
22
24
 
23
25
  EXEC_TOOLS_SNAPSHOT_PATH = (
@@ -140,6 +142,20 @@ class ToolRegistry:
140
142
  tool_type=call.tool_type,
141
143
  )
142
144
  except Exception as exc: # pragma: no cover - defensive wrapper
145
+ if (debug_dir := get_debug_dir()) is not None:
146
+ with (debug_dir / "tool_errors.jsonl").open("a", encoding="utf-8") as handle:
147
+ handle.write(
148
+ json.dumps(
149
+ {
150
+ "tool": call.name,
151
+ "call_id": call.call_id,
152
+ "error": f"{type(exc).__name__}: {exc}",
153
+ "traceback": traceback.format_exc(),
154
+ },
155
+ ensure_ascii=False,
156
+ )
157
+ )
158
+ handle.write("\n")
143
159
  return ToolResult(
144
160
  call_id=call.call_id,
145
161
  name=call.name,
@@ -1,4 +1,5 @@
1
1
  from .dotenv import DOTENV_FILENAME, load_codex_dotenv, parse_dotenv, parse_dotenv_value
2
+ from .debug import get_debug_dir
2
3
  from .get_env import build_user_agent, get_shell_name, get_timezone_name
3
4
  from .random_ids import uuid7_string
4
5
  from .compactor import DEFAULT_COMPACT_PROMPT, SUMMARY_PREFIX, compact
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "format_cli_plan_messages",
32
33
  "format_cli_tool_call_message",
33
34
  "format_cli_tool_message",
35
+ "get_debug_dir",
34
36
  "get_shell_name",
35
37
  "get_timezone_name",
36
38
  "load_codex_dotenv",
@@ -0,0 +1,12 @@
1
+ import os
2
+ from pathlib import Path
3
+ import typing
4
+
5
+
6
+ def get_debug_dir() -> 'typing.Union[Path, None]':
7
+ value = os.environ.get("PYCODEX_DEBUG_LOG", "").strip()
8
+ if not value:
9
+ return None
10
+ path = Path(value).expanduser()
11
+ path.mkdir(parents=True, exist_ok=True)
12
+ return path
@@ -158,13 +158,29 @@ class Spinner:
158
158
  self._paused = False
159
159
 
160
160
  def clear(self) -> 'None':
161
- if not self._enabled or not self._visible:
162
- return
163
161
  with self._terminal_lock:
162
+ if not self._visible:
163
+ return
164
164
  self._raw_write("\r\x1b[2K")
165
165
  self._raw_flush()
166
166
  self._visible = False
167
167
 
168
+ def render_now(self) -> 'None':
169
+ if not self._turn_active or self._paused:
170
+ return
171
+ frame = colorize_cli_message(
172
+ build_cli_spinner_frame(self._index, self._label),
173
+ "status",
174
+ self._color_enabled,
175
+ )
176
+ self._index += 1
177
+ with self._terminal_lock:
178
+ if not self._turn_active or self._paused:
179
+ return
180
+ self._raw_write(f"\r\x1b[2K{frame}")
181
+ self._raw_flush()
182
+ self._visible = True
183
+
168
184
  def close(self) -> 'None':
169
185
  self.finish_turn()
170
186
  if self._thread is not None:
@@ -726,6 +742,7 @@ class CliSessionView:
726
742
  else:
727
743
  self._spinner.resume()
728
744
  self._spinner.set_label("running provider tools")
745
+ self._spinner.render_now()
729
746
  return
730
747
 
731
748
  if event.kind == "tool_started":
@@ -745,15 +762,11 @@ class CliSessionView:
745
762
  self._spinner.set_label(f"running {tool_name}")
746
763
  else:
747
764
  self._spinner.set_label("running provider tools")
765
+ self._spinner.render_now()
748
766
  return
749
767
 
750
768
  if event.kind == "tool_completed":
751
769
  self._finish_stream()
752
- if self._input_active:
753
- self._spinner.pause()
754
- else:
755
- self._spinner.resume()
756
- self._spinner.set_label("thinking")
757
770
  tool_name, summary, is_error = extract_tool_event_display(event.payload)
758
771
  summary = self._rewrite_agent_summary(tool_name, summary)
759
772
  if tool_name == "update_plan" and not is_error:
@@ -762,6 +775,12 @@ class CliSessionView:
762
775
  self._print_line(
763
776
  colorize_cli_message(line, "plan", self._color_enabled)
764
777
  )
778
+ if self._input_active:
779
+ self._spinner.pause()
780
+ else:
781
+ self._spinner.resume()
782
+ self._spinner.set_label("thinking")
783
+ self._spinner.render_now()
765
784
  return
766
785
  message = format_cli_tool_message(
767
786
  tool_name,
@@ -770,6 +789,12 @@ class CliSessionView:
770
789
  )
771
790
  self._remember_agent_name(tool_name, summary)
772
791
  self._print_line(self._colorize_formatted_tool_message(message))
792
+ if self._input_active:
793
+ self._spinner.pause()
794
+ else:
795
+ self._spinner.resume()
796
+ self._spinner.set_label("thinking")
797
+ self._spinner.render_now()
773
798
  return
774
799
 
775
800
  if event.kind == "turn_completed":
@@ -830,6 +855,8 @@ class CliSessionView:
830
855
 
831
856
  def resume_spinner(self) -> 'None':
832
857
  self._spinner.resume()
858
+ if not self._input_active:
859
+ self._spinner.render_now()
833
860
 
834
861
  def set_input_active(self, active: 'bool', resume_spinner: 'bool' = True) -> 'None':
835
862
  self._input_active = active
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-codex"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "A minimal Python extraction of Codex's main agent loop"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6.2"
@@ -55,12 +55,18 @@ def build_parser() -> 'argparse.ArgumentParser':
55
55
  prog="python -m responses_server",
56
56
  description=(
57
57
  "Standalone localhost `/v1/responses` server that translates the "
58
- "Codex/Responses subset onto an outcomming `/v1/chat/completions` backend."
58
+ "Codex/Responses subset onto an outcomming `/v1/chat/completions` "
59
+ "or `/v1/messages` backend."
59
60
  ),
60
61
  )
61
62
  parser.add_argument("--host", default="127.0.0.1")
62
63
  parser.add_argument("--port", type=int, default=8001)
63
64
  parser.add_argument("--outcomming-base-url", required=True)
65
+ parser.add_argument(
66
+ "--outcomming-api",
67
+ default="chat_completions",
68
+ choices=["chat_completions", "messages"],
69
+ )
64
70
  parser.add_argument("--outcomming-api-key-env", default=None)
65
71
  parser.add_argument("--model-provider", default=None)
66
72
  parser.add_argument("--timeout-seconds", type=float, default=120.0)
@@ -80,10 +86,12 @@ def launch_chat_completion_compat_server(
80
86
  base_url: 'str',
81
87
  api_key_env: 'typing.Union[str, None]' = None,
82
88
  model_provider: 'typing.Union[str, None]' = None,
89
+ outcomming_api: 'str' = "chat_completions",
83
90
  ):
84
91
  config = CompatServerConfig.from_base_url(
85
92
  base_url,
86
93
  api_key_env,
94
+ outcomming_api=outcomming_api,
87
95
  model_provider=model_provider,
88
96
  )
89
97
  server = ManagedResponseServer(config)
@@ -209,6 +217,7 @@ def main() -> 'None':
209
217
  host=args.host,
210
218
  port=args.port,
211
219
  outcomming_base_url=args.outcomming_base_url,
220
+ outcomming_api=args.outcomming_api,
212
221
  outcomming_api_key_env=args.outcomming_api_key_env,
213
222
  model_provider=args.model_provider,
214
223
  timeout_seconds=args.timeout_seconds,
@@ -10,6 +10,7 @@ class CompatServerConfig:
10
10
  host: 'str' = "127.0.0.1"
11
11
  port: 'int' = 0
12
12
  outcomming_base_url: 'str' = "http://127.0.0.1:8000/v1"
13
+ outcomming_api: 'str' = "chat_completions"
13
14
  outcomming_api_key_env: 'typing.Union[str, None]' = None
14
15
  model_provider: 'typing.Union[str, None]' = None
15
16
  timeout_seconds: 'float' = 120.0
@@ -24,15 +25,24 @@ class CompatServerConfig:
24
25
  base = self.outcomming_base_url.rstrip("/")
25
26
  return f"{base}/chat/completions"
26
27
 
28
+ def outcomming_messages_url(self) -> 'str':
29
+ base = self.outcomming_base_url.rstrip("/")
30
+ return f"{base}/messages"
31
+
27
32
  def outcomming_models_url(self) -> 'str':
28
33
  base = self.outcomming_base_url.rstrip("/")
29
34
  return f"{base}/models"
30
35
 
36
+ def normalized_outcomming_api(self) -> 'str':
37
+ value = str(self.outcomming_api or "").strip().lower()
38
+ return value or "chat_completions"
39
+
31
40
  def with_ephemeral_port(self) -> 'CompatServerConfig':
32
41
  return CompatServerConfig(
33
42
  host=self.host,
34
43
  port=0,
35
44
  outcomming_base_url=self.outcomming_base_url,
45
+ outcomming_api=self.outcomming_api,
36
46
  outcomming_api_key_env=self.outcomming_api_key_env,
37
47
  model_provider=self.model_provider,
38
48
  timeout_seconds=self.timeout_seconds,
@@ -44,6 +54,7 @@ class CompatServerConfig:
44
54
  outcomming_base_url: 'str',
45
55
  api_key_env: 'typing.Union[str, None]' = None,
46
56
  model_provider: 'typing.Union[str, None]' = None,
57
+ outcomming_api: 'str' = "chat_completions",
47
58
  ) -> 'CompatServerConfig':
48
59
  parsed = urllib.parse.urlparse(outcomming_base_url)
49
60
  if not parsed.scheme or not parsed.netloc:
@@ -58,6 +69,7 @@ class CompatServerConfig:
58
69
  )
59
70
  return cls(
60
71
  outcomming_base_url=outcomming_base_url,
72
+ outcomming_api=outcomming_api,
61
73
  outcomming_api_key_env=api_key_env,
62
74
  model_provider=model_provider,
63
75
  )