python-codex 0.1.1__tar.gz → 0.1.2__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 (97) hide show
  1. python_codex-0.1.2/.github/workflows/test.yml +65 -0
  2. {python_codex-0.1.1 → python_codex-0.1.2}/PKG-INFO +1 -1
  3. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/cli.py +8 -1
  4. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/runtime_services.py +3 -0
  5. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/exec_tool.py +1 -1
  6. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/unified_exec_manager.py +19 -2
  7. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/utils/get_env.py +23 -4
  8. {python_codex-0.1.1 → python_codex-0.1.2}/pyproject.toml +2 -2
  9. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_agent.py +24 -7
  10. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_builtin_tools.py +34 -2
  11. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_cli.py +32 -1
  12. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_model.py +24 -0
  13. {python_codex-0.1.1 → python_codex-0.1.2}/.github/workflows/publish.yml +0 -0
  14. {python_codex-0.1.1 → python_codex-0.1.2}/.gitignore +0 -0
  15. {python_codex-0.1.1 → python_codex-0.1.2}/AGENTS.md +0 -0
  16. {python_codex-0.1.1 → python_codex-0.1.2}/LICENSE +0 -0
  17. {python_codex-0.1.1 → python_codex-0.1.2}/README.md +0 -0
  18. {python_codex-0.1.1 → python_codex-0.1.2}/README_ZH.md +0 -0
  19. {python_codex-0.1.1 → python_codex-0.1.2}/docs/ALIGNMENT.md +0 -0
  20. {python_codex-0.1.1 → python_codex-0.1.2}/docs/CONTEXT.md +0 -0
  21. {python_codex-0.1.1 → python_codex-0.1.2}/docs/responses_server/README.md +0 -0
  22. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/__init__.py +0 -0
  23. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/agent.py +0 -0
  24. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/collaboration.py +0 -0
  25. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/context.py +0 -0
  26. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/doctor.py +0 -0
  27. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/model.py +0 -0
  28. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/portable.py +0 -0
  29. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/portable_server.py +0 -0
  30. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/collaboration_default.md +0 -0
  31. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/collaboration_plan.md +0 -0
  32. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/default_base_instructions.md +0 -0
  33. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/exec_tools.json +0 -0
  34. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/models.json +0 -0
  35. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
  36. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
  37. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
  38. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
  39. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
  40. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
  41. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
  42. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
  43. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/prompts/subagent_tools.json +0 -0
  44. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/protocol.py +0 -0
  45. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/runtime.py +0 -0
  46. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/__init__.py +0 -0
  47. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/agent_tool_schemas.py +0 -0
  48. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/apply_patch_tool.py +0 -0
  49. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/base_tool.py +0 -0
  50. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/close_agent_tool.py +0 -0
  51. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/code_mode_manager.py +0 -0
  52. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/exec_command_tool.py +0 -0
  53. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/exec_runtime.js +0 -0
  54. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/grep_files_tool.py +0 -0
  55. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/list_dir_tool.py +0 -0
  56. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/read_file_tool.py +0 -0
  57. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/request_permissions_tool.py +0 -0
  58. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/request_user_input_tool.py +0 -0
  59. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/resume_agent_tool.py +0 -0
  60. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/send_input_tool.py +0 -0
  61. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/shell_command_tool.py +0 -0
  62. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/shell_tool.py +0 -0
  63. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/spawn_agent_tool.py +0 -0
  64. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/update_plan_tool.py +0 -0
  65. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/view_image_tool.py +0 -0
  66. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/wait_agent_tool.py +0 -0
  67. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/wait_tool.py +0 -0
  68. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/web_search_tool.py +0 -0
  69. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/tools/write_stdin_tool.py +0 -0
  70. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/utils/__init__.py +0 -0
  71. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/utils/dotenv.py +0 -0
  72. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/utils/random_ids.py +0 -0
  73. {python_codex-0.1.1 → python_codex-0.1.2}/pycodex/utils/visualize.py +0 -0
  74. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/__init__.py +0 -0
  75. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/__main__.py +0 -0
  76. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/app.py +0 -0
  77. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/config.py +0 -0
  78. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/payload_processors.py +0 -0
  79. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/server.py +0 -0
  80. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/session_store.py +0 -0
  81. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/stream_router.py +0 -0
  82. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/tools/__init__.py +0 -0
  83. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/tools/custom_adapter.py +0 -0
  84. {python_codex-0.1.1 → python_codex-0.1.2}/responses_server/tools/web_search.py +0 -0
  85. {python_codex-0.1.1 → python_codex-0.1.2}/tests/TESTS.md +0 -0
  86. {python_codex-0.1.1 → python_codex-0.1.2}/tests/__init__.py +0 -0
  87. {python_codex-0.1.1 → python_codex-0.1.2}/tests/compare_request_user_input_roundtrip.py +0 -0
  88. {python_codex-0.1.1 → python_codex-0.1.2}/tests/compare_steer_request_bodies.py +0 -0
  89. {python_codex-0.1.1 → python_codex-0.1.2}/tests/compare_tool_schemas.py +0 -0
  90. {python_codex-0.1.1 → python_codex-0.1.2}/tests/fake_responses_server.py +0 -0
  91. {python_codex-0.1.1 → python_codex-0.1.2}/tests/fakes.py +0 -0
  92. {python_codex-0.1.1 → python_codex-0.1.2}/tests/responses_server/fake_chat_completions_server.py +0 -0
  93. {python_codex-0.1.1 → python_codex-0.1.2}/tests/responses_server/test_server.py +0 -0
  94. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_context.py +0 -0
  95. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_doctor.py +0 -0
  96. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_fake_responses_server.py +0 -0
  97. {python_codex-0.1.1 → python_codex-0.1.2}/tests/test_portable.py +0 -0
@@ -0,0 +1,65 @@
1
+ name: test
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ jobs:
11
+ pytest-py310:
12
+ name: pytest (Python 3.10)
13
+ runs-on: ubuntu-22.04
14
+ steps:
15
+ - name: Check out repository
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.10"
22
+
23
+ - name: Set up uv
24
+ uses: astral-sh/setup-uv@v5
25
+
26
+ - name: Sync dependencies
27
+ run: uv sync --dev
28
+
29
+ - name: Run pytest
30
+ run: uv run pytest
31
+
32
+ pytest-py36:
33
+ name: pytest (Python 3.6)
34
+ runs-on: ubuntu-22.04
35
+ container:
36
+ image: python:3.6.15-slim-bullseye
37
+ steps:
38
+ - name: Install system dependencies
39
+ run: |
40
+ apt-get update
41
+ apt-get install -y --no-install-recommends git
42
+
43
+ - name: Check out repository
44
+ uses: actions/checkout@v4
45
+
46
+ - name: Mark workspace as safe for git
47
+ run: git config --global --add safe.directory "$PWD"
48
+
49
+ - name: Set up Node.js
50
+ uses: actions/setup-node@v4
51
+ with:
52
+ node-version: "20"
53
+
54
+ - name: Set up uv
55
+ uses: astral-sh/setup-uv@v5
56
+
57
+ - name: Show runtime versions
58
+ run: python --version
59
+ - run: node --version
60
+
61
+ - name: Sync dependencies
62
+ run: uv sync --dev
63
+
64
+ - name: Run pytest
65
+ run: uv run pytest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-codex
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A minimal Python extraction of Codex's main agent loop
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -21,7 +21,6 @@ from .protocol import AgentEvent
21
21
  from .runtime import AgentRuntime
22
22
  from .runtime_services import RuntimeEnvironment, create_runtime_environment
23
23
  from .utils import CliSessionView, load_codex_dotenv
24
- from responses_server import launch_chat_completion_compat_server
25
24
 
26
25
  EXIT_COMMANDS = {"/exit", "/quit"}
27
26
  HISTORY_COMMAND = "/history"
@@ -33,6 +32,14 @@ LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
33
32
  CLI_ORIGINATOR = "codex-tui"
34
33
 
35
34
 
35
+ def launch_chat_completion_compat_server(*args, **kwargs):
36
+ from responses_server import (
37
+ launch_chat_completion_compat_server as launch_compat_server,
38
+ )
39
+
40
+ return launch_compat_server(*args, **kwargs)
41
+
42
+
36
43
  def configure_loguru() -> None:
37
44
  try:
38
45
  from loguru import logger
@@ -346,6 +346,9 @@ class SubAgentManager:
346
346
  managed.state = "completed"
347
347
  finally:
348
348
  managed.pending_submission_ids.discard(submission_id)
349
+ if managed.pending_submission_ids and managed.error_message is None:
350
+ managed.completed_message = None
351
+ managed.state = "running"
349
352
  async with self._condition:
350
353
  self._condition.notify_all()
351
354
 
@@ -17,7 +17,7 @@ from ..protocol import JSONValue
17
17
  from .base_tool import BaseTool, ToolContext
18
18
  from .code_mode_manager import CodeModeManager
19
19
 
20
- EXEC_FREEFORM_GRAMMAR = """start: pragma_source | plain_source
20
+ EXEC_FREEFORM_GRAMMAR = r"""start: pragma_source | plain_source
21
21
  pragma_source: PRAGMA_LINE NEWLINE SOURCE
22
22
  plain_source: SOURCE
23
23
 
@@ -184,6 +184,9 @@ class _HeadTailBuffer:
184
184
  self.tail.clear()
185
185
  return combined
186
186
 
187
+ def has_data(self) -> bool:
188
+ return bool(self.head or self.tail)
189
+
187
190
 
188
191
  @dataclass(slots=True)
189
192
  class UnifiedExecSession:
@@ -194,6 +197,7 @@ class UnifiedExecSession:
194
197
  tty: bool
195
198
  unread_output: _HeadTailBuffer = field(default_factory=_HeadTailBuffer)
196
199
  reader_task: asyncio.Task | None = None
200
+ output_event: asyncio.Event = field(default_factory=asyncio.Event)
197
201
 
198
202
 
199
203
  class UnifiedExecManager:
@@ -294,11 +298,22 @@ class UnifiedExecManager:
294
298
  if session is None:
295
299
  return f"Error: session_id {session_id} is not running."
296
300
 
297
- start_wait = asyncio.get_running_loop().time()
301
+ loop = asyncio.get_running_loop()
302
+ start_wait = loop.time()
298
303
  try:
299
304
  await asyncio.wait_for(session.process.wait(), timeout=yield_time_ms / 1000.0)
300
305
  except asyncio.TimeoutError:
301
- pass
306
+ remaining_seconds = (yield_time_ms / 1000.0) - (loop.time() - start_wait)
307
+ if (
308
+ session.process.returncode is None
309
+ and not session.unread_output.has_data()
310
+ and remaining_seconds > 0
311
+ ):
312
+ session.output_event.clear()
313
+ try:
314
+ await asyncio.wait_for(session.output_event.wait(), timeout=remaining_seconds)
315
+ except asyncio.TimeoutError:
316
+ pass
302
317
 
303
318
  if session.reader_task is not None and session.process.returncode is not None:
304
319
  await session.reader_task
@@ -345,6 +360,8 @@ class UnifiedExecManager:
345
360
  if not chunk:
346
361
  break
347
362
  session.unread_output.push_chunk(chunk)
363
+ session.output_event.set()
364
+ session.output_event.set()
348
365
 
349
366
  def _resolve_workdir(self, workdir: str | None) -> Path:
350
367
  if not workdir:
@@ -83,10 +83,15 @@ def get_package_version() -> str:
83
83
  detected = _detect_upstream_codex_version()
84
84
  if detected is not None:
85
85
  return detected
86
- try:
87
- return importlib.metadata.version("pycodex")
88
- except importlib.metadata.PackageNotFoundError:
89
- return "0.1.0"
86
+ for distribution_name in ("python-codex", "pycodex"):
87
+ try:
88
+ return importlib.metadata.version(distribution_name)
89
+ except importlib.metadata.PackageNotFoundError:
90
+ continue
91
+ local_version = _read_local_package_version()
92
+ if local_version is not None:
93
+ return local_version
94
+ return "0.1.0"
90
95
 
91
96
 
92
97
  def get_os_info() -> tuple[str, str]:
@@ -178,6 +183,20 @@ def _normalize_os_version(version: str) -> str:
178
183
  return version
179
184
 
180
185
 
186
+ def _read_local_package_version() -> str | None:
187
+ pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
188
+ if not pyproject_path.is_file():
189
+ return None
190
+ match = re.search(
191
+ r'^\s*version\s*=\s*"([^"]+)"\s*$',
192
+ pyproject_path.read_text(encoding="utf-8"),
193
+ flags=re.MULTILINE,
194
+ )
195
+ if match is None:
196
+ return None
197
+ return match.group(1).strip() or None
198
+
199
+
181
200
  def _tmux_display_message(fmt: str) -> str | None:
182
201
  try:
183
202
  output = subprocess.run(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-codex"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "A minimal Python extraction of Codex's main agent loop"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -34,7 +34,7 @@ package = true
34
34
  default-groups = []
35
35
 
36
36
  [tool.hatch.build.targets.wheel]
37
- packages = ["pycodex"]
37
+ packages = ["pycodex", "responses_server"]
38
38
 
39
39
  [tool.pytest.ini_options]
40
40
  pythonpath = ["."]
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import time
5
4
 
6
5
  import pytest
7
6
 
@@ -44,6 +43,25 @@ class SlowTool(BaseTool):
44
43
  return "done"
45
44
 
46
45
 
46
+ class CoordinatedParallelTool(BaseTool):
47
+ description = "Tool that proves both calls entered before either finished."
48
+ input_schema = {"type": "object"}
49
+ supports_parallel = True
50
+
51
+ def __init__(self, name: str, entered: list[str], both_started: asyncio.Event) -> None:
52
+ self.name = name
53
+ self._entered = entered
54
+ self._both_started = both_started
55
+
56
+ async def run(self, context, args):
57
+ del context, args
58
+ self._entered.append(self.name)
59
+ if len(self._entered) == 2:
60
+ self._both_started.set()
61
+ await asyncio.wait_for(self._both_started.wait(), timeout=0.2)
62
+ return "done"
63
+
64
+
47
65
  class WaitAgentNotificationTool(BaseTool):
48
66
  name = "wait_agent"
49
67
  description = "Returns a completed sub-agent status."
@@ -109,17 +127,16 @@ async def test_parallel_tools_share_one_model_round() -> None:
109
127
  )
110
128
 
111
129
  tools = ToolRegistry()
112
- tools.register(SlowTool("slow_a"))
113
- tools.register(SlowTool("slow_b"))
130
+ entered: list[str] = []
131
+ both_started = asyncio.Event()
132
+ tools.register(CoordinatedParallelTool("slow_a", entered, both_started))
133
+ tools.register(CoordinatedParallelTool("slow_b", entered, both_started))
114
134
 
115
135
  agent = AgentLoop(model, tools)
116
-
117
- started = time.perf_counter()
118
136
  result = await agent.run_turn(["并行跑两个工具"])
119
- elapsed = time.perf_counter() - started
120
137
 
121
138
  assert result.output_text == "两个工具都执行完了"
122
- assert elapsed < 0.11
139
+ assert entered == ["slow_a", "slow_b"] or entered == ["slow_b", "slow_a"]
123
140
 
124
141
 
125
142
  @pytest.mark.asyncio
@@ -259,7 +259,9 @@ async def test_exec_command_tool_returns_session_for_long_running_process(tmp_pa
259
259
 
260
260
  assert closed.is_error is False
261
261
  assert "Process exited with code 0" in closed.output
262
- assert "startend" in closed.output
262
+ combined_output = result.output + closed.output
263
+ assert "start" in combined_output
264
+ assert "end" in combined_output
263
265
 
264
266
 
265
267
  @pytest.mark.asyncio
@@ -374,9 +376,27 @@ async def test_write_stdin_tool_defaults_to_upstream_truncation_budget(tmp_path)
374
376
  ToolContext(turn_id="turn_9_write_finish", history=()),
375
377
  )
376
378
 
379
+ body = poll.output.split("Output:\n", 1)[1]
380
+ for _ in range(5):
381
+ if "tokens truncated" in body:
382
+ break
383
+ if "Process exited with code 0" in poll.output:
384
+ break
385
+ poll = await registry.execute(
386
+ ToolCall(
387
+ call_id="call_9_write_finish_poll",
388
+ name="write_stdin",
389
+ arguments={
390
+ "session_id": session_id,
391
+ "yield_time_ms": 200,
392
+ },
393
+ ),
394
+ ToolContext(turn_id="turn_9_write_finish_poll", history=()),
395
+ )
396
+ body = poll.output.split("Output:\n", 1)[1]
397
+
377
398
  assert poll.is_error is False
378
399
  assert "Original token count: " in poll.output
379
- body = poll.output.split("Output:\n", 1)[1]
380
400
  assert body.startswith("Total output lines: 1\n\nHEAD")
381
401
  assert "tokens truncated" in body
382
402
  assert body.endswith("TAIL")
@@ -899,6 +919,18 @@ async def test_spawn_agent_send_input_wait_and_close_round_trip() -> None:
899
919
  ),
900
920
  ToolContext(turn_id="turn_wait", history=()),
901
921
  )
922
+ if waited.output == {
923
+ "status": {agent_id: {"completed": "done one"}},
924
+ "timed_out": False,
925
+ }:
926
+ waited = await registry.execute(
927
+ ToolCall(
928
+ call_id="call_wait_again",
929
+ name="wait_agent",
930
+ arguments={"ids": [agent_id], "timeout_ms": 1000},
931
+ ),
932
+ ToolContext(turn_id="turn_wait_again", history=()),
933
+ )
902
934
 
903
935
  assert waited.output == {
904
936
  "status": {agent_id: {"completed": "done two"}},
@@ -344,9 +344,23 @@ def test_build_runtime_overrides_provider_for_managed_vllm_mode(
344
344
  @pytest.mark.asyncio
345
345
  async def test_run_cli_launches_managed_responses_server_for_vllm_endpoint(
346
346
  monkeypatch,
347
+ tmp_path,
347
348
  ) -> None:
348
349
  started = {}
349
350
  registered = {}
351
+ config_path = tmp_path / "config.toml"
352
+ config_path.write_text(
353
+ "\n".join(
354
+ [
355
+ 'model = "demo-model"',
356
+ 'model_provider = "demo"',
357
+ '[model_providers.demo]',
358
+ 'base_url = "https://example.com/v1"',
359
+ 'env_key = "DUMMY_KEY"',
360
+ ]
361
+ )
362
+ )
363
+ monkeypatch.setenv("DUMMY_KEY", "test-key")
350
364
 
351
365
  class _FakeManagedServer:
352
366
  base_url = "http://127.0.0.1:18001/v1"
@@ -415,6 +429,8 @@ async def test_run_cli_launches_managed_responses_server_for_vllm_endpoint(
415
429
 
416
430
  args = build_parser().parse_args(
417
431
  [
432
+ "--config",
433
+ str(config_path),
418
434
  "--vllm-endpoint",
419
435
  "http://127.0.0.1:18000",
420
436
  "Reply with exactly OK.",
@@ -2588,7 +2604,22 @@ async def test_prompt_request_permissions_supports_session_scope() -> None:
2588
2604
  async def test_run_cli_returns_non_zero_on_single_turn_error(
2589
2605
  monkeypatch: pytest.MonkeyPatch,
2590
2606
  capsys: pytest.CaptureFixture[str],
2607
+ tmp_path: Path,
2591
2608
  ) -> None:
2609
+ config_path = tmp_path / "config.toml"
2610
+ config_path.write_text(
2611
+ "\n".join(
2612
+ [
2613
+ 'model = "demo-model"',
2614
+ 'model_provider = "demo"',
2615
+ '[model_providers.demo]',
2616
+ 'base_url = "https://example.com/v1"',
2617
+ 'env_key = "DUMMY_KEY"',
2618
+ ]
2619
+ )
2620
+ )
2621
+ monkeypatch.setenv("DUMMY_KEY", "test-key")
2622
+
2592
2623
  class _FakeRuntime:
2593
2624
  def __init__(self):
2594
2625
  self._stopped = asyncio.Event()
@@ -2609,7 +2640,7 @@ async def test_run_cli_returns_non_zero_on_single_turn_error(
2609
2640
  monkeypatch.setattr("pycodex.cli.build_runtime", lambda *args, **kwargs: _FakeRuntime())
2610
2641
  monkeypatch.setattr("sys.stdin.read", lambda: "")
2611
2642
 
2612
- args = build_parser().parse_args(["hello"])
2643
+ args = build_parser().parse_args(["--config", str(config_path), "hello"])
2613
2644
  code = await run_cli(args)
2614
2645
 
2615
2646
  assert code == 1
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
 
9
9
  import pytest
10
10
 
11
+ import pycodex.utils.get_env as get_env
11
12
  from pycodex import (
12
13
  AssistantMessage,
13
14
  ContextMessage,
@@ -541,6 +542,29 @@ def test_responses_model_client_builds_tui_user_agent(monkeypatch) -> None:
541
542
  assert re.match(r'^codex-tui/.+ \(.+; .+\) .+ \(codex-tui; .+\)$', headers['user-agent'])
542
543
 
543
544
 
545
+ def test_get_package_version_reads_distribution_name(monkeypatch) -> None:
546
+ def fake_version(name: str) -> str:
547
+ if name == 'python-codex':
548
+ return '0.1.2'
549
+ raise get_env.importlib.metadata.PackageNotFoundError
550
+
551
+ monkeypatch.setattr(get_env, '_detect_upstream_codex_version', lambda: None)
552
+ monkeypatch.setattr(get_env.importlib.metadata, 'version', fake_version)
553
+
554
+ assert get_env.get_package_version() == '0.1.2'
555
+
556
+
557
+ def test_get_package_version_falls_back_to_local_pyproject(monkeypatch) -> None:
558
+ def fake_missing_version(_name: str) -> str:
559
+ raise get_env.importlib.metadata.PackageNotFoundError
560
+
561
+ monkeypatch.setattr(get_env, '_detect_upstream_codex_version', lambda: None)
562
+ monkeypatch.setattr(get_env.importlib.metadata, 'version', fake_missing_version)
563
+ monkeypatch.setattr(get_env, '_read_local_package_version', lambda: '0.1.2')
564
+
565
+ assert get_env.get_package_version() == '0.1.2'
566
+
567
+
544
568
  def test_responses_model_client_serializes_prompt_turn_metadata(monkeypatch) -> None:
545
569
  provider = ResponsesProviderConfig(
546
570
  model='demo-model',
File without changes
File without changes
File without changes
File without changes
File without changes