python-codex 0.1.9__tar.gz → 0.1.10__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.1.9 → python_codex-0.1.10}/AGENTS.md +2 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/PKG-INFO +1 -1
- {python_codex-0.1.9 → python_codex-0.1.10}/docs/responses_server/README.md +14 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/context.py +17 -12
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/model.py +1 -1
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/portable.py +4 -3
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/base_tool.py +2 -1
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/dotenv.py +3 -1
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/get_env.py +5 -1
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/session_persist.py +4 -2
- {python_codex-0.1.9 → python_codex-0.1.10}/pyproject.toml +1 -1
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/messages_api.py +2 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/payload_processors.py +1 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/server.py +6 -4
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/stream_router.py +20 -3
- python_codex-0.1.10/responses_server/trajectory_dump.py +105 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/responses_server/fake_chat_completions_server.py +24 -13
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/responses_server/test_server.py +198 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_cli.py +67 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/.github/workflows/publish.yml +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/.github/workflows/test.yml +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/.gitignore +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/LICENSE +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/README.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/README_ZH.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/docs/ALIGNMENT.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/docs/CONTEXT.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/__init__.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/agent.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/cli.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/collaboration.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/compat.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/doctor.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/portable_server.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/collaboration_default.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/collaboration_plan.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/default_base_instructions.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/exec_tools.json +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/models.json +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/subagent_tools.json +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/protocol.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/runtime.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/runtime_services.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/__init__.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/agent_tool_schemas.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/apply_patch_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/close_agent_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/code_mode_manager.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/exec_command_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/exec_runtime.js +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/exec_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/grep_files_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/list_dir_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/read_file_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/request_permissions_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/request_user_input_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/resume_agent_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/send_input_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/shell_command_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/shell_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/spawn_agent_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/unified_exec_manager.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/update_plan_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/view_image_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/wait_agent_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/wait_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/web_search_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/tools/write_stdin_tool.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/__init__.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/compactor.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/debug.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/random_ids.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/pycodex/utils/visualize.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/__init__.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/__main__.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/app.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/config.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/session_store.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/tools/__init__.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/tools/custom_adapter.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/responses_server/tools/web_search.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/TESTS.md +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/__init__.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/compare_request_user_input_roundtrip.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/compare_steer_request_bodies.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/compare_tool_schemas.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/fake_responses_server.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/fakes.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_agent.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_builtin_tools.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_compactor.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_context.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_doctor.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_fake_responses_server.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_model.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_portable.py +0 -0
- {python_codex-0.1.9 → python_codex-0.1.10}/tests/test_py36_syntax.py +0 -0
|
@@ -15,6 +15,7 @@
|
|
|
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
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
17
|
- 真实 vLLM `0.19.0` 的 `/v1/messages` 会对缺失 `max_tokens` 直接返回 `400`;messages 适配层必须总是补这个字段。当前约定是优先透传请求里的 `max_output_tokens`/`max_tokens`,否则回退到默认 `32000`。
|
|
18
|
+
- 对 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`”重建即可。
|
|
18
19
|
- `pycodex` 默认是最小交互 CLI;无 prompt 时进入 REPL,并通过 `AgentRuntime` 跑外层提交循环。当前会显示最小事件流、assistant 流式输出、简单 title/history(`/title`, `/history`),并默认注册一组与原版一一对应的本地工具子集。
|
|
19
20
|
- 交互 CLI 的事件流展示优先表达用户可感知的阶段(例如工具开始/完成、模型回看工具结果),不要直接把内部 `iteration` 计数暴露成主要状态文案;`iterations` 应继续保留在 `TurnResult` 等程序化结果里。
|
|
20
21
|
- prompt/context 相关逻辑统一放在 `pycodex/context.py`:`AgentLoop` 只维护真实会话历史;每轮请求前由 `ContextManager` 注入 base instructions、developer message、`AGENTS.md` 指令和 `<environment_context>`,且这些注入项不写回 history。
|
|
@@ -49,5 +50,6 @@
|
|
|
49
50
|
- `--put` 的 CLI UX 现在约定为“先打印打包文件清单和上传目标,再打印结果”,并且最后一行保留为可直接执行的 `pycodex --call SECRET-CALLID@<host:port>` 一键启动命令;后续如果再改输出,尽量保留这个末行语义。
|
|
50
51
|
- `--put` 现在不是“只上传就结束”:上传成功后还会立刻跑一次真实的 `--call` 下载/解包 round-trip 测试,只有这个测试也成功才算整个 `--put` 成功。排障时如果上传成功但 CLI 仍退出非零,要继续看 call 路径而不只看 put 端。
|
|
51
52
|
- `--put` 现在会先做 `/healthz` preflight,再开始扫描/压缩目录;如果用户报“卡死”,先看目标 `host:port` 是否真有 storage server 在监听。默认打包策略也已经切到白名单:只带 `config.toml`、`.env`、`AGENTS.md`、`AGENTS.override.md`、`skills/**`,以及 config 里相对引用的 `model_instructions_file`,所以像 `sessions/` 这类运行态目录不会再靠黑名单排除。
|
|
53
|
+
- `--call` / portable storage paths must not rely on the process default text encoding. Always pass `encoding="utf-8"` when reading config, prompts, AGENTS files, skills, dotenv, and session history; for user-authored instructions/history, prefer `errors="replace"` so a Windows GBK locale cannot crash on UTF-8 punctuation such as U+2264 or em dash.
|
|
52
54
|
- 对接真实 `~/.codex/sessions/.../rollout-*.jsonl` 时,不要假设它一定是严格的一行一个 JSON object:本机样本可能包含 pretty-printed 多行对象,且文件尾部偶尔带未完成记录。恢复历史时用 concatenated-JSON 方式读取,并容忍尾部残缺。
|
|
53
55
|
- `pycodex` 本地 session 保存现在也按上游思路走:新 session 一开始就分配稳定的 uuidv7 thread/session id,并把历史增量追加到 `CODEX_HOME/sessions/.../rollout-*.jsonl`;`/resume` 列表应只展示至少有真实 user message 的 rollout,避免空白新 session 污染恢复列表。
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- vLLM chat-completions `reasoning` / `reasoning_content` -> Responses `reasoning` item 适配
|
|
27
27
|
- vLLM 历史 `reasoning` item -> assistant message `reasoning` 字段回放
|
|
28
28
|
- vLLM streaming `usage` -> final `response.completed.response.usage`
|
|
29
|
+
- 当环境变量 `PYCODEX_DUMP` 存在时,为每条 outcomming 请求附加 `return_token_ids = true`,并把抓到的 `prompt_token_ids` / `token_ids` 以 JSONL 追加到 `{PYCODEX_DUMP}/dump.jsonl`
|
|
29
30
|
- 下游 chat stream 如果半路断开,会转成上游可解析的 `response.failed` 事件,而不是直接截断 HTTP body
|
|
30
31
|
- 普通 function tools
|
|
31
32
|
- custom tools 的 function-wrapper 兼容适配
|
|
@@ -76,6 +77,19 @@ uv run python -m responses_server \
|
|
|
76
77
|
默认会在本地启动一个 incomming Responses 服务;真正监听地址由 `--host` 和 `--port`
|
|
77
78
|
控制。
|
|
78
79
|
|
|
80
|
+
如果要导出下游 trajectory token id,可以在启动前设置:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export PYCODEX_DUMP=/tmp/pycodex-dump
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
server 会为每条实际转发到下游的请求附上 `return_token_ids = true`,并把
|
|
87
|
+
trajectory 追加到 `${PYCODEX_DUMP}/dump.jsonl`,当前记录格式是:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{"tokens":{"prefill":[1,2,3],"decode":[4,5,6]},"send_timestamp":2222.0}
|
|
91
|
+
```
|
|
92
|
+
|
|
79
93
|
如果下游 provider 需要对 chat payload 做定制化改写,可以在
|
|
80
94
|
`responses_server/payload_processors.py` 里注册对应 `model_provider -> proc_fn`
|
|
81
95
|
映射;server 会在真正发出每一条 outcomming `/v1/chat/completions` 请求前,
|
|
@@ -89,7 +89,7 @@ class ContextConfig:
|
|
|
89
89
|
profile: 'typing.Union[str, None]' = None,
|
|
90
90
|
) -> 'ContextConfig':
|
|
91
91
|
path = Path(config_path)
|
|
92
|
-
data = tomllib.loads(path.read_text())
|
|
92
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
93
93
|
selected = dict(data)
|
|
94
94
|
if profile is not None:
|
|
95
95
|
overrides = data.get("profiles", {}).get(profile)
|
|
@@ -162,7 +162,9 @@ class ContextManager:
|
|
|
162
162
|
self._include_permissions_instructions = include_permissions_instructions
|
|
163
163
|
self._include_skills_instructions = include_skills_instructions
|
|
164
164
|
self._network_access = network_access
|
|
165
|
-
self._default_base_instructions = DEFAULT_BASE_INSTRUCTIONS_PATH.read_text(
|
|
165
|
+
self._default_base_instructions = DEFAULT_BASE_INSTRUCTIONS_PATH.read_text(
|
|
166
|
+
encoding="utf-8"
|
|
167
|
+
)
|
|
166
168
|
self._workspace_metadata_turn_id: 'typing.Union[str, None]' = None
|
|
167
169
|
self._workspace_metadata_cache: 'typing.Union[JSONDict, None]' = None
|
|
168
170
|
|
|
@@ -237,7 +239,10 @@ class ContextManager:
|
|
|
237
239
|
if self._config.base_instructions is not None:
|
|
238
240
|
return self._config.base_instructions
|
|
239
241
|
if self._config.model_instructions_file is not None:
|
|
240
|
-
return self._config.model_instructions_file.read_text(
|
|
242
|
+
return self._config.model_instructions_file.read_text(
|
|
243
|
+
encoding="utf-8",
|
|
244
|
+
errors="replace",
|
|
245
|
+
).strip()
|
|
241
246
|
resolved = self._resolve_model_instructions()
|
|
242
247
|
if resolved is not None:
|
|
243
248
|
return resolved
|
|
@@ -327,11 +332,11 @@ class ContextManager:
|
|
|
327
332
|
return None
|
|
328
333
|
|
|
329
334
|
sandbox_text = (
|
|
330
|
-
sandbox_prompt_path.read_text().strip().replace(
|
|
335
|
+
sandbox_prompt_path.read_text(encoding="utf-8").strip().replace(
|
|
331
336
|
"{network_access}", self._network_access
|
|
332
337
|
)
|
|
333
338
|
)
|
|
334
|
-
approval_text = approval_prompt_path.read_text().strip()
|
|
339
|
+
approval_text = approval_prompt_path.read_text(encoding="utf-8").strip()
|
|
335
340
|
return "\n".join(
|
|
336
341
|
[
|
|
337
342
|
PERMISSIONS_OPEN_TAG,
|
|
@@ -429,7 +434,7 @@ class ContextManager:
|
|
|
429
434
|
docs: 'typing.List[str]' = []
|
|
430
435
|
remaining = self._config.project_doc_max_bytes
|
|
431
436
|
for path in self._discover_project_doc_paths():
|
|
432
|
-
text = path.read_text()
|
|
437
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
433
438
|
if not text.strip():
|
|
434
439
|
continue
|
|
435
440
|
if remaining is None:
|
|
@@ -437,7 +442,7 @@ class ContextManager:
|
|
|
437
442
|
continue
|
|
438
443
|
if remaining <= 0:
|
|
439
444
|
break
|
|
440
|
-
encoded = text.encode()
|
|
445
|
+
encoded = text.encode("utf-8")
|
|
441
446
|
docs.append(encoded[:remaining].decode(errors="ignore"))
|
|
442
447
|
remaining -= min(len(encoded), remaining)
|
|
443
448
|
if not docs:
|
|
@@ -507,15 +512,15 @@ def _normalize_int(value) -> 'typing.Union[int, None]':
|
|
|
507
512
|
|
|
508
513
|
def _default_collaboration_instructions(mode: 'CollaborationMode') -> 'str':
|
|
509
514
|
if mode == "plan":
|
|
510
|
-
return PLAN_COLLABORATION_INSTRUCTIONS_PATH.read_text()
|
|
511
|
-
return DEFAULT_COLLABORATION_INSTRUCTIONS_PATH.read_text()
|
|
515
|
+
return PLAN_COLLABORATION_INSTRUCTIONS_PATH.read_text(encoding="utf-8")
|
|
516
|
+
return DEFAULT_COLLABORATION_INSTRUCTIONS_PATH.read_text(encoding="utf-8")
|
|
512
517
|
|
|
513
518
|
|
|
514
519
|
def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
|
|
515
520
|
for candidate_name in (LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME):
|
|
516
521
|
candidate = base / candidate_name
|
|
517
522
|
try:
|
|
518
|
-
contents = candidate.read_text()
|
|
523
|
+
contents = candidate.read_text(encoding="utf-8", errors="replace")
|
|
519
524
|
except OSError:
|
|
520
525
|
continue
|
|
521
526
|
trimmed = contents.strip()
|
|
@@ -526,7 +531,7 @@ def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
|
|
|
526
531
|
|
|
527
532
|
@lru_cache(maxsize=1)
|
|
528
533
|
def _load_models_by_slug() -> 'typing.Dict[str, JSONDict]':
|
|
529
|
-
payload = json.loads(DEFAULT_MODELS_PATH.read_text())
|
|
534
|
+
payload = json.loads(DEFAULT_MODELS_PATH.read_text(encoding="utf-8"))
|
|
530
535
|
models = payload.get("models", [])
|
|
531
536
|
by_slug: 'typing.Dict[str, JSONDict]' = {}
|
|
532
537
|
for model in models:
|
|
@@ -571,7 +576,7 @@ def _discover_skill_files(
|
|
|
571
576
|
|
|
572
577
|
|
|
573
578
|
def _parse_skill_descriptor(path: 'Path', scope_rank: 'int') -> 'typing.Union[SkillDescriptor, None]':
|
|
574
|
-
text = path.read_text()
|
|
579
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
575
580
|
if not text.startswith("---\n"):
|
|
576
581
|
return None
|
|
577
582
|
end_marker = "\n---\n"
|
|
@@ -71,7 +71,7 @@ class ResponsesProviderConfig:
|
|
|
71
71
|
config_path: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
|
|
72
72
|
profile: 'typing.Union[str, None]' = None,
|
|
73
73
|
) -> 'ResponsesProviderConfig':
|
|
74
|
-
data = tomllib.loads(Path(config_path).read_text())
|
|
74
|
+
data = tomllib.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
75
75
|
selected = dict(data)
|
|
76
76
|
if profile is not None:
|
|
77
77
|
overrides = data.get("profiles", {}).get(profile)
|
|
@@ -123,7 +123,8 @@ def bootstrap_called_home(
|
|
|
123
123
|
},
|
|
124
124
|
ensure_ascii=False,
|
|
125
125
|
indent=2,
|
|
126
|
-
)
|
|
126
|
+
),
|
|
127
|
+
encoding="utf-8",
|
|
127
128
|
)
|
|
128
129
|
return home_dir / DEFAULT_ENTRY_CONFIG
|
|
129
130
|
|
|
@@ -199,7 +200,7 @@ def _collect_config_referenced_files(root: 'Path') -> 'typing.Set[str]':
|
|
|
199
200
|
config_path = root / DEFAULT_ENTRY_CONFIG
|
|
200
201
|
if not config_path.is_file():
|
|
201
202
|
return set()
|
|
202
|
-
data = tomllib.loads(config_path.read_text())
|
|
203
|
+
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
203
204
|
referenced: 'typing.Set[str]' = set()
|
|
204
205
|
candidates = [data]
|
|
205
206
|
profiles = data.get("profiles")
|
|
@@ -352,7 +353,7 @@ def _load_cached_metadata(metadata_path: 'Path') -> 'typing.Dict[str, object]':
|
|
|
352
353
|
if not metadata_path.is_file():
|
|
353
354
|
return {}
|
|
354
355
|
try:
|
|
355
|
-
payload = json.loads(metadata_path.read_text())
|
|
356
|
+
payload = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
356
357
|
except (ValueError, OSError):
|
|
357
358
|
return {}
|
|
358
359
|
return payload if isinstance(payload, dict) else {}
|
|
@@ -30,7 +30,8 @@ EXEC_TOOLS_SNAPSHOT_PATH = (
|
|
|
30
30
|
@lru_cache(maxsize=1)
|
|
31
31
|
def _load_exec_tool_payloads() -> 'typing.Dict[str, JSONDict]':
|
|
32
32
|
payloads: 'typing.Dict[str, JSONDict]' = {}
|
|
33
|
-
|
|
33
|
+
raw_payloads = EXEC_TOOLS_SNAPSHOT_PATH.read_text(encoding="utf-8")
|
|
34
|
+
for payload in json.loads(raw_payloads):
|
|
34
35
|
if not isinstance(payload, dict):
|
|
35
36
|
continue
|
|
36
37
|
name = payload.get("name")
|
|
@@ -18,7 +18,9 @@ def load_codex_dotenv(config_path: 'typing.Union[str, Path]') -> 'None':
|
|
|
18
18
|
_LOADED_CODEX_DOTENV_HOMES.add(codex_home)
|
|
19
19
|
return
|
|
20
20
|
|
|
21
|
-
for key, value in parse_dotenv(
|
|
21
|
+
for key, value in parse_dotenv(
|
|
22
|
+
dotenv_path.read_text(encoding="utf-8", errors="replace")
|
|
23
|
+
).items():
|
|
22
24
|
if key.upper().startswith(ILLEGAL_ENV_VAR_PREFIX):
|
|
23
25
|
continue
|
|
24
26
|
os.environ[key] = value
|
|
@@ -98,7 +98,11 @@ def get_os_info() -> 'typing.Tuple[str, str]':
|
|
|
98
98
|
os_release = Path("/etc/os-release")
|
|
99
99
|
if os_release.is_file():
|
|
100
100
|
values: 'typing.Dict[str, str]' = {}
|
|
101
|
-
|
|
101
|
+
os_release_text = os_release.read_text(
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
errors="replace",
|
|
104
|
+
)
|
|
105
|
+
for line in os_release_text.splitlines():
|
|
102
106
|
if "=" not in line:
|
|
103
107
|
continue
|
|
104
108
|
key, value = line.split("=", 1)
|
|
@@ -282,7 +282,9 @@ def _latest_thread_names_by_id(codex_home: 'Path') -> 'typing.Dict[str, str]':
|
|
|
282
282
|
return {}
|
|
283
283
|
|
|
284
284
|
names_by_id: 'typing.Dict[str, str]' = {}
|
|
285
|
-
for raw_line in reversed(
|
|
285
|
+
for raw_line in reversed(
|
|
286
|
+
index_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
287
|
+
):
|
|
286
288
|
line = raw_line.strip()
|
|
287
289
|
if not line:
|
|
288
290
|
continue
|
|
@@ -321,7 +323,7 @@ def _extract_first_user_message_preview(rollout_path: 'Path') -> 'typing.Union[s
|
|
|
321
323
|
|
|
322
324
|
|
|
323
325
|
def _iter_rollout_entries(rollout_path: 'Path') -> 'typing.Iterable[typing.Dict[str, object]]':
|
|
324
|
-
text = rollout_path.read_text()
|
|
326
|
+
text = rollout_path.read_text(encoding="utf-8", errors="replace")
|
|
325
327
|
decoder = json.JSONDecoder()
|
|
326
328
|
index = 0
|
|
327
329
|
parsed_entries = 0
|
|
@@ -66,6 +66,8 @@ def build_messages_request(
|
|
|
66
66
|
"max_tokens": _resolve_max_tokens(outcomming_request),
|
|
67
67
|
"stream": bool(outcomming_request.get("stream", True)),
|
|
68
68
|
}
|
|
69
|
+
if isinstance(outcomming_request.get("return_token_ids"), bool):
|
|
70
|
+
payload["return_token_ids"] = bool(outcomming_request.get("return_token_ids"))
|
|
69
71
|
if system_blocks:
|
|
70
72
|
payload["system"] = system_blocks
|
|
71
73
|
|
|
@@ -32,6 +32,7 @@ class OutgoingRequest(TypedDict):
|
|
|
32
32
|
tools: 'Optional[typing.List[typing.Dict[str, object]]]'
|
|
33
33
|
tool_choice: 'Optional[object]'
|
|
34
34
|
parallel_tool_calls: 'Optional[bool]'
|
|
35
|
+
return_token_ids: 'Optional[bool]'
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
PayloadPostProcessor = Callable[[OutgoingRequest], OutgoingRequest]
|
|
@@ -3,6 +3,7 @@ from .config import CompatServerConfig
|
|
|
3
3
|
from .payload_processors import post_process_outcomming_request
|
|
4
4
|
from .session_store import SessionStore
|
|
5
5
|
from .stream_router import StreamRouter
|
|
6
|
+
from .trajectory_dump import TrajectoryDumpWriter
|
|
6
7
|
import typing
|
|
7
8
|
|
|
8
9
|
|
|
@@ -16,6 +17,7 @@ class ResponseServer:
|
|
|
16
17
|
self._config = config
|
|
17
18
|
self._session_store = session_store or SessionStore()
|
|
18
19
|
self._stream_router = stream_router or StreamRouter(config)
|
|
20
|
+
self._trajectory_dump = TrajectoryDumpWriter.from_env()
|
|
19
21
|
|
|
20
22
|
@property
|
|
21
23
|
def config(self) -> 'CompatServerConfig':
|
|
@@ -38,6 +40,9 @@ class ResponseServer:
|
|
|
38
40
|
request_headers: 'typing.Dict[str, str]',
|
|
39
41
|
):
|
|
40
42
|
outcomming_request = self._stream_router.build_outcomming_request(request_body)
|
|
43
|
+
if self._trajectory_dump is not None:
|
|
44
|
+
# vLLM surfaces prompt/decode token IDs only when this flag is set.
|
|
45
|
+
outcomming_request["return_token_ids"] = True
|
|
41
46
|
outcomming_request = post_process_outcomming_request(
|
|
42
47
|
outcomming_request,
|
|
43
48
|
self._config.model_provider,
|
|
@@ -52,12 +57,9 @@ class ResponseServer:
|
|
|
52
57
|
session_id=session_id,
|
|
53
58
|
model=str(outcomming_request["model"]),
|
|
54
59
|
)
|
|
55
|
-
incomming_stream = self._stream_router.open_outcomming_stream(
|
|
56
|
-
outcomming_request
|
|
57
|
-
)
|
|
58
60
|
return self._stream_router.route_stream(
|
|
59
|
-
incomming_stream,
|
|
60
61
|
stored_response,
|
|
61
62
|
outcomming_request,
|
|
62
63
|
custom_tool_names,
|
|
64
|
+
self._trajectory_dump,
|
|
63
65
|
)
|
|
@@ -27,6 +27,7 @@ from .tools.web_search import (
|
|
|
27
27
|
hydrate_tool_call_names,
|
|
28
28
|
partition_tool_calls,
|
|
29
29
|
)
|
|
30
|
+
from .trajectory_dump import TrajectoryDumpWriter
|
|
30
31
|
import typing
|
|
31
32
|
|
|
32
33
|
|
|
@@ -285,10 +286,10 @@ class StreamRouter:
|
|
|
285
286
|
|
|
286
287
|
def route_stream(
|
|
287
288
|
self,
|
|
288
|
-
incomming_stream,
|
|
289
289
|
stored_response: 'StoredResponse',
|
|
290
290
|
outcomming_request: 'typing.Dict[str, object]',
|
|
291
291
|
custom_tool_names: 'typing.Union[typing.Set[str], None]' = None,
|
|
292
|
+
trajectory_dump: 'typing.Union[TrajectoryDumpWriter, None]' = None,
|
|
292
293
|
):
|
|
293
294
|
yield (
|
|
294
295
|
"response.created",
|
|
@@ -307,7 +308,10 @@ class StreamRouter:
|
|
|
307
308
|
reasoning_parts: 'typing.List[str]' = []
|
|
308
309
|
latest_usage: 'typing.Dict[str, object]' = {}
|
|
309
310
|
current_request = json.loads(json.dumps(outcomming_request))
|
|
310
|
-
current_stream =
|
|
311
|
+
current_stream = self._open_tracked_outcomming_stream(
|
|
312
|
+
current_request,
|
|
313
|
+
trajectory_dump,
|
|
314
|
+
)
|
|
311
315
|
|
|
312
316
|
while True:
|
|
313
317
|
tool_calls: 'typing.Dict[int, typing.Dict[str, object]]' = {}
|
|
@@ -352,7 +356,10 @@ class StreamRouter:
|
|
|
352
356
|
)
|
|
353
357
|
except ValueError as exc:
|
|
354
358
|
raise OutcommingChatError(str(exc)) from exc
|
|
355
|
-
current_stream = self.
|
|
359
|
+
current_stream = self._open_tracked_outcomming_stream(
|
|
360
|
+
current_request,
|
|
361
|
+
trajectory_dump,
|
|
362
|
+
)
|
|
356
363
|
continue
|
|
357
364
|
|
|
358
365
|
for item in self._build_output_items(
|
|
@@ -394,6 +401,16 @@ class StreamRouter:
|
|
|
394
401
|
},
|
|
395
402
|
)
|
|
396
403
|
|
|
404
|
+
def _open_tracked_outcomming_stream(
|
|
405
|
+
self,
|
|
406
|
+
outcomming_request: 'typing.Dict[str, object]',
|
|
407
|
+
trajectory_dump: 'typing.Union[TrajectoryDumpWriter, None]' = None,
|
|
408
|
+
):
|
|
409
|
+
outcomming_stream = self.open_outcomming_stream(outcomming_request)
|
|
410
|
+
if trajectory_dump is None:
|
|
411
|
+
return outcomming_stream
|
|
412
|
+
return trajectory_dump.wrap_stream(outcomming_stream)
|
|
413
|
+
|
|
397
414
|
def _responses_input_to_chat_messages(
|
|
398
415
|
self,
|
|
399
416
|
instructions: 'str',
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TrajectoryDumpWriter:
|
|
10
|
+
ENV_VAR = "PYCODEX_DUMP"
|
|
11
|
+
|
|
12
|
+
def __init__(self, root_dir: 'str') -> 'None':
|
|
13
|
+
self._root_dir = os.path.abspath(root_dir)
|
|
14
|
+
self._dump_path = os.path.join(self._root_dir, "dump.jsonl")
|
|
15
|
+
self._lock = threading.Lock()
|
|
16
|
+
os.makedirs(self._root_dir, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_env(cls) -> 'typing.Union[TrajectoryDumpWriter, None]':
|
|
20
|
+
root_dir = str(os.environ.get(cls.ENV_VAR, "") or "").strip()
|
|
21
|
+
if not root_dir:
|
|
22
|
+
return None
|
|
23
|
+
return cls(root_dir)
|
|
24
|
+
|
|
25
|
+
def wrap_stream(self, outcomming_stream):
|
|
26
|
+
def iter_stream():
|
|
27
|
+
capture = _TrajectoryCapture(self, time.time())
|
|
28
|
+
try:
|
|
29
|
+
for chunk in outcomming_stream:
|
|
30
|
+
capture.observe_chunk(chunk)
|
|
31
|
+
yield chunk
|
|
32
|
+
finally:
|
|
33
|
+
capture.flush()
|
|
34
|
+
|
|
35
|
+
return iter_stream()
|
|
36
|
+
|
|
37
|
+
def _append_record(self, record: 'typing.Dict[str, object]') -> 'None':
|
|
38
|
+
serialized = json.dumps(record, ensure_ascii=False)
|
|
39
|
+
with self._lock:
|
|
40
|
+
os.makedirs(self._root_dir, exist_ok=True)
|
|
41
|
+
with open(self._dump_path, "a", encoding="utf-8") as handle:
|
|
42
|
+
handle.write(serialized)
|
|
43
|
+
handle.write("\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _TrajectoryCapture:
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
writer: 'TrajectoryDumpWriter',
|
|
50
|
+
send_timestamp: 'float',
|
|
51
|
+
) -> 'None':
|
|
52
|
+
self._writer = writer
|
|
53
|
+
self._send_timestamp = float(send_timestamp)
|
|
54
|
+
self._prefill_token_ids = None
|
|
55
|
+
self._decode_token_ids = []
|
|
56
|
+
self._closed = False
|
|
57
|
+
|
|
58
|
+
def observe_chunk(self, payload: 'object') -> 'None':
|
|
59
|
+
if not isinstance(payload, dict):
|
|
60
|
+
return
|
|
61
|
+
if self._prefill_token_ids is None and "prompt_token_ids" in payload:
|
|
62
|
+
normalized_prefill = _normalize_token_ids(payload.get("prompt_token_ids"))
|
|
63
|
+
if normalized_prefill is not None:
|
|
64
|
+
self._prefill_token_ids = normalized_prefill
|
|
65
|
+
|
|
66
|
+
choices = payload.get("choices") or []
|
|
67
|
+
if not isinstance(choices, list):
|
|
68
|
+
return
|
|
69
|
+
for raw_choice in choices:
|
|
70
|
+
if not isinstance(raw_choice, dict):
|
|
71
|
+
continue
|
|
72
|
+
normalized_decode = _normalize_token_ids(raw_choice.get("token_ids"))
|
|
73
|
+
if normalized_decode:
|
|
74
|
+
self._decode_token_ids.extend(normalized_decode)
|
|
75
|
+
|
|
76
|
+
def flush(self) -> 'None':
|
|
77
|
+
if self._closed:
|
|
78
|
+
return
|
|
79
|
+
self._closed = True
|
|
80
|
+
record = {
|
|
81
|
+
"tokens": {
|
|
82
|
+
"prefill": list(self._prefill_token_ids or []),
|
|
83
|
+
"decode": list(self._decode_token_ids),
|
|
84
|
+
},
|
|
85
|
+
"send_timestamp": self._send_timestamp,
|
|
86
|
+
}
|
|
87
|
+
try:
|
|
88
|
+
self._writer._append_record(record)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
print(
|
|
91
|
+
"responses_server: failed to append PYCODEX_DUMP trajectory: %s"
|
|
92
|
+
% exc,
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _normalize_token_ids(raw_value: 'object') -> 'typing.Union[typing.List[int], None]':
|
|
98
|
+
if not isinstance(raw_value, list):
|
|
99
|
+
return None
|
|
100
|
+
token_ids = []
|
|
101
|
+
for value in raw_value:
|
|
102
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
103
|
+
continue
|
|
104
|
+
token_ids.append(value)
|
|
105
|
+
return token_ids
|
{python_codex-0.1.9 → python_codex-0.1.10}/tests/responses_server/fake_chat_completions_server.py
RENAMED
|
@@ -102,20 +102,31 @@ class RunningFastAPITestServer:
|
|
|
102
102
|
raise RuntimeError("timed out waiting for fake FastAPI server to stop")
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def build_text_chunks(
|
|
105
|
+
def build_text_chunks(
|
|
106
|
+
text: 'str',
|
|
107
|
+
model_id: 'str' = DEFAULT_MODEL_ID,
|
|
108
|
+
prompt_token_ids: 'typing.Union[typing.List[int], None]' = None,
|
|
109
|
+
decode_token_ids: 'typing.Union[typing.List[int], None]' = None,
|
|
110
|
+
) -> 'typing.List[typing.Dict[str, object]]':
|
|
111
|
+
first_chunk: 'typing.Dict[str, object]' = {
|
|
112
|
+
"id": "chatcmpl_mock",
|
|
113
|
+
"object": "chat.completion.chunk",
|
|
114
|
+
"model": model_id,
|
|
115
|
+
"choices": [
|
|
116
|
+
{
|
|
117
|
+
"index": 0,
|
|
118
|
+
"delta": {"role": "assistant", "content": text},
|
|
119
|
+
"finish_reason": None,
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
if prompt_token_ids is not None:
|
|
124
|
+
first_chunk["prompt_token_ids"] = list(prompt_token_ids)
|
|
125
|
+
if decode_token_ids is not None:
|
|
126
|
+
first_chunk["choices"][0]["token_ids"] = list(decode_token_ids)
|
|
127
|
+
|
|
106
128
|
return [
|
|
107
|
-
|
|
108
|
-
"id": "chatcmpl_mock",
|
|
109
|
-
"object": "chat.completion.chunk",
|
|
110
|
-
"model": model_id,
|
|
111
|
-
"choices": [
|
|
112
|
-
{
|
|
113
|
-
"index": 0,
|
|
114
|
-
"delta": {"role": "assistant", "content": text},
|
|
115
|
-
"finish_reason": None,
|
|
116
|
-
}
|
|
117
|
-
],
|
|
118
|
-
},
|
|
129
|
+
first_chunk,
|
|
119
130
|
{
|
|
120
131
|
"id": "chatcmpl_mock",
|
|
121
132
|
"object": "chat.completion.chunk",
|
|
@@ -82,6 +82,80 @@ def test_responses_server_streams_text_from_chat_backend(tmp_path) -> 'None':
|
|
|
82
82
|
]
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def test_responses_server_dumps_forwarded_chat_token_trajectory(
|
|
86
|
+
tmp_path,
|
|
87
|
+
monkeypatch,
|
|
88
|
+
) -> 'None':
|
|
89
|
+
dump_root = tmp_path / "dump"
|
|
90
|
+
monkeypatch.setenv("PYCODEX_DUMP", str(dump_root))
|
|
91
|
+
capture_store = CaptureStore(tmp_path / "chat_capture")
|
|
92
|
+
fake_chat_server = build_fake_chat_server(
|
|
93
|
+
capture_store,
|
|
94
|
+
build_text_chunks(
|
|
95
|
+
"Hello",
|
|
96
|
+
prompt_token_ids=[101, 102, 103],
|
|
97
|
+
decode_token_ids=[201, 202],
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
fake_chat_server.start()
|
|
101
|
+
|
|
102
|
+
app = ManagedResponseServer.build_app(
|
|
103
|
+
CompatServerConfig(
|
|
104
|
+
outcomming_base_url=f"http://127.0.0.1:{fake_chat_server.server_port}/v1",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
with TestClient(app) as client:
|
|
110
|
+
response = client.post(
|
|
111
|
+
"/v1/responses",
|
|
112
|
+
json={
|
|
113
|
+
"model": "gpt-5.4",
|
|
114
|
+
"instructions": "Be concise.",
|
|
115
|
+
"input": [
|
|
116
|
+
{
|
|
117
|
+
"type": "message",
|
|
118
|
+
"role": "user",
|
|
119
|
+
"content": [{"type": "input_text", "text": "hi"}],
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"tools": [],
|
|
123
|
+
"tool_choice": "auto",
|
|
124
|
+
"parallel_tool_calls": True,
|
|
125
|
+
"stream": True,
|
|
126
|
+
},
|
|
127
|
+
headers={"Accept": "text/event-stream"},
|
|
128
|
+
)
|
|
129
|
+
status = response.status_code
|
|
130
|
+
finally:
|
|
131
|
+
fake_chat_server.stop()
|
|
132
|
+
|
|
133
|
+
assert status == 200
|
|
134
|
+
|
|
135
|
+
request_files = sorted((tmp_path / "chat_capture").glob("*_POST_*.json"))
|
|
136
|
+
assert len(request_files) == 1
|
|
137
|
+
request = json.loads(request_files[0].read_text())
|
|
138
|
+
assert request["body"]["return_token_ids"] is True
|
|
139
|
+
|
|
140
|
+
dump_file = dump_root / "dump.jsonl"
|
|
141
|
+
assert dump_file.exists()
|
|
142
|
+
dump_records = [
|
|
143
|
+
json.loads(line)
|
|
144
|
+
for line in dump_file.read_text().splitlines()
|
|
145
|
+
if line.strip()
|
|
146
|
+
]
|
|
147
|
+
assert dump_records == [
|
|
148
|
+
{
|
|
149
|
+
"tokens": {
|
|
150
|
+
"prefill": [101, 102, 103],
|
|
151
|
+
"decode": [201, 202],
|
|
152
|
+
},
|
|
153
|
+
"send_timestamp": dump_records[0]["send_timestamp"],
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
assert isinstance(dump_records[0]["send_timestamp"], float)
|
|
157
|
+
|
|
158
|
+
|
|
85
159
|
def test_responses_server_streams_text_from_messages_backend(tmp_path) -> 'None':
|
|
86
160
|
capture_store = CaptureStore(tmp_path / "messages_capture")
|
|
87
161
|
fake_messages_server = build_fake_messages_server(
|
|
@@ -1547,6 +1621,130 @@ def test_responses_server_mocks_web_search_and_continues_chat(tmp_path) -> 'None
|
|
|
1547
1621
|
}
|
|
1548
1622
|
|
|
1549
1623
|
|
|
1624
|
+
def test_responses_server_dumps_all_forwarded_requests_for_mock_web_search(
|
|
1625
|
+
tmp_path,
|
|
1626
|
+
monkeypatch,
|
|
1627
|
+
) -> 'None':
|
|
1628
|
+
dump_root = tmp_path / "dump"
|
|
1629
|
+
monkeypatch.setenv("PYCODEX_DUMP", str(dump_root))
|
|
1630
|
+
capture_store = CaptureStore(tmp_path / "chat_capture")
|
|
1631
|
+
fake_chat_server = build_fake_chat_server(
|
|
1632
|
+
capture_store,
|
|
1633
|
+
[
|
|
1634
|
+
[
|
|
1635
|
+
{
|
|
1636
|
+
"id": "chatcmpl_mock",
|
|
1637
|
+
"object": "chat.completion.chunk",
|
|
1638
|
+
"model": "gpt-5.4",
|
|
1639
|
+
"prompt_token_ids": [11, 12],
|
|
1640
|
+
"choices": [
|
|
1641
|
+
{
|
|
1642
|
+
"index": 0,
|
|
1643
|
+
"delta": {
|
|
1644
|
+
"tool_calls": [
|
|
1645
|
+
{
|
|
1646
|
+
"index": 0,
|
|
1647
|
+
"id": "ws_1",
|
|
1648
|
+
"type": "function",
|
|
1649
|
+
"function": {
|
|
1650
|
+
"arguments": '{"query":"github codex"}'
|
|
1651
|
+
},
|
|
1652
|
+
}
|
|
1653
|
+
]
|
|
1654
|
+
},
|
|
1655
|
+
"token_ids": [21, 22],
|
|
1656
|
+
"finish_reason": None,
|
|
1657
|
+
}
|
|
1658
|
+
],
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
"id": "chatcmpl_mock",
|
|
1662
|
+
"object": "chat.completion.chunk",
|
|
1663
|
+
"model": "gpt-5.4",
|
|
1664
|
+
"choices": [
|
|
1665
|
+
{
|
|
1666
|
+
"index": 0,
|
|
1667
|
+
"delta": {},
|
|
1668
|
+
"finish_reason": "tool_calls",
|
|
1669
|
+
}
|
|
1670
|
+
],
|
|
1671
|
+
},
|
|
1672
|
+
],
|
|
1673
|
+
build_text_chunks(
|
|
1674
|
+
"done",
|
|
1675
|
+
prompt_token_ids=[31, 32, 33],
|
|
1676
|
+
decode_token_ids=[41, 42],
|
|
1677
|
+
),
|
|
1678
|
+
],
|
|
1679
|
+
)
|
|
1680
|
+
fake_chat_server.start()
|
|
1681
|
+
|
|
1682
|
+
app = ManagedResponseServer.build_app(
|
|
1683
|
+
CompatServerConfig(
|
|
1684
|
+
outcomming_base_url=f"http://127.0.0.1:{fake_chat_server.server_port}/v1",
|
|
1685
|
+
)
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
try:
|
|
1689
|
+
with TestClient(app) as client:
|
|
1690
|
+
response = client.post(
|
|
1691
|
+
"/v1/responses",
|
|
1692
|
+
json={
|
|
1693
|
+
"model": "gpt-5.4",
|
|
1694
|
+
"instructions": "Be concise.",
|
|
1695
|
+
"input": [
|
|
1696
|
+
{
|
|
1697
|
+
"type": "message",
|
|
1698
|
+
"role": "user",
|
|
1699
|
+
"content": [
|
|
1700
|
+
{
|
|
1701
|
+
"type": "input_text",
|
|
1702
|
+
"text": "Search the web, then answer.",
|
|
1703
|
+
}
|
|
1704
|
+
],
|
|
1705
|
+
}
|
|
1706
|
+
],
|
|
1707
|
+
"tools": [
|
|
1708
|
+
{
|
|
1709
|
+
"type": "web_search",
|
|
1710
|
+
"external_web_access": True,
|
|
1711
|
+
}
|
|
1712
|
+
],
|
|
1713
|
+
"tool_choice": "auto",
|
|
1714
|
+
"parallel_tool_calls": False,
|
|
1715
|
+
"stream": True,
|
|
1716
|
+
},
|
|
1717
|
+
headers={"Accept": "text/event-stream"},
|
|
1718
|
+
)
|
|
1719
|
+
status = response.status_code
|
|
1720
|
+
finally:
|
|
1721
|
+
fake_chat_server.stop()
|
|
1722
|
+
|
|
1723
|
+
assert status == 200
|
|
1724
|
+
|
|
1725
|
+
request_files = sorted((tmp_path / "chat_capture").glob("*_POST_*.json"))
|
|
1726
|
+
assert len(request_files) == 2
|
|
1727
|
+
for request_file in request_files:
|
|
1728
|
+
request = json.loads(request_file.read_text())
|
|
1729
|
+
assert request["body"]["return_token_ids"] is True
|
|
1730
|
+
|
|
1731
|
+
dump_records = [
|
|
1732
|
+
json.loads(line)
|
|
1733
|
+
for line in (dump_root / "dump.jsonl").read_text().splitlines()
|
|
1734
|
+
if line.strip()
|
|
1735
|
+
]
|
|
1736
|
+
assert len(dump_records) == 2
|
|
1737
|
+
assert dump_records[0]["tokens"] == {
|
|
1738
|
+
"prefill": [11, 12],
|
|
1739
|
+
"decode": [21, 22],
|
|
1740
|
+
}
|
|
1741
|
+
assert dump_records[1]["tokens"] == {
|
|
1742
|
+
"prefill": [31, 32, 33],
|
|
1743
|
+
"decode": [41, 42],
|
|
1744
|
+
}
|
|
1745
|
+
assert dump_records[0]["send_timestamp"] <= dump_records[1]["send_timestamp"]
|
|
1746
|
+
|
|
1747
|
+
|
|
1550
1748
|
def test_responses_server_turns_mock_web_search_calls_into_messages_followup(
|
|
1551
1749
|
tmp_path,
|
|
1552
1750
|
) -> 'None':
|
|
@@ -773,6 +773,73 @@ async def test_run_cli_bootstraps_called_home_before_loading_config(
|
|
|
773
773
|
assert captured["provider_config"].api_key_env == "PORTABLE_API_KEY"
|
|
774
774
|
|
|
775
775
|
|
|
776
|
+
@pytest.mark.asyncio
|
|
777
|
+
async def test_run_cli_call_reads_called_home_text_as_utf8(
|
|
778
|
+
tmp_path,
|
|
779
|
+
monkeypatch,
|
|
780
|
+
) -> 'None':
|
|
781
|
+
codex_home = tmp_path / "codex-home"
|
|
782
|
+
codex_home.mkdir()
|
|
783
|
+
_write_stored_codex_home(codex_home)
|
|
784
|
+
(codex_home / "AGENTS.md").write_text(
|
|
785
|
+
"stored rules with unicode: \u22645 min \u2014 \u4e2d\u6587\n",
|
|
786
|
+
encoding="utf-8",
|
|
787
|
+
)
|
|
788
|
+
(codex_home / "skills" / "demo" / "SKILL.md").write_text(
|
|
789
|
+
"---\n"
|
|
790
|
+
"name: demo\n"
|
|
791
|
+
"description: unicode \u2264 \u2014 \u4e2d\u6587\n"
|
|
792
|
+
"---\n"
|
|
793
|
+
"Stored skill.\n",
|
|
794
|
+
encoding="utf-8",
|
|
795
|
+
)
|
|
796
|
+
server = CodexStorageServer(tmp_path / "storage-server", port=0)
|
|
797
|
+
server.start()
|
|
798
|
+
|
|
799
|
+
original_read_text = Path.read_text
|
|
800
|
+
|
|
801
|
+
def read_text_as_gbk_by_default(path, *args, **kwargs):
|
|
802
|
+
encoding = kwargs.get("encoding")
|
|
803
|
+
if args:
|
|
804
|
+
encoding = args[0]
|
|
805
|
+
if encoding is None:
|
|
806
|
+
return path.read_bytes().decode("gbk")
|
|
807
|
+
return original_read_text(path, *args, **kwargs)
|
|
808
|
+
|
|
809
|
+
class _FakeResponsesModelClient(_ScriptedResponsesClient):
|
|
810
|
+
def __init__(
|
|
811
|
+
self,
|
|
812
|
+
config,
|
|
813
|
+
timeout_seconds,
|
|
814
|
+
session_id=None,
|
|
815
|
+
originator=None,
|
|
816
|
+
user_agent=None,
|
|
817
|
+
openai_subagent=None,
|
|
818
|
+
) -> 'None':
|
|
819
|
+
del timeout_seconds, user_agent, openai_subagent
|
|
820
|
+
super().__init__([ModelResponse(items=[AssistantMessage(text="OK")])])
|
|
821
|
+
self._config = config
|
|
822
|
+
self.model = config.model
|
|
823
|
+
self._session_id = session_id
|
|
824
|
+
self._originator = originator or "pycodex"
|
|
825
|
+
|
|
826
|
+
monkeypatch.setattr(Path, "read_text", read_text_as_gbk_by_default)
|
|
827
|
+
monkeypatch.setattr("pycodex.cli.ResponsesModelClient", _FakeResponsesModelClient)
|
|
828
|
+
monkeypatch.setattr("pycodex.cli.configure_loguru", lambda: None)
|
|
829
|
+
monkeypatch.setattr("sys.stdin.read", lambda: "")
|
|
830
|
+
monkeypatch.delenv("CODEX_HOME", raising=False)
|
|
831
|
+
monkeypatch.delenv("PORTABLE_API_KEY", raising=False)
|
|
832
|
+
|
|
833
|
+
try:
|
|
834
|
+
stored_call = upload_codex_home(f"{codex_home}@{server.server_address}")
|
|
835
|
+
args = build_parser().parse_args(["--call", stored_call, "say ok"])
|
|
836
|
+
exit_code = await run_cli(args)
|
|
837
|
+
finally:
|
|
838
|
+
server.stop()
|
|
839
|
+
|
|
840
|
+
assert exit_code == 0
|
|
841
|
+
|
|
842
|
+
|
|
776
843
|
def test_get_tools_registers_expected_builtin_tools() -> 'None':
|
|
777
844
|
registry = get_tools()
|
|
778
845
|
assert registry.names() == (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/never.md
RENAMED
|
File without changes
|
{python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/on_failure.md
RENAMED
|
File without changes
|
{python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/approval_policy/on_request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_codex-0.1.9 → python_codex-0.1.10}/pycodex/prompts/permissions/sandbox_mode/read_only.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|