python-codex 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
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.
- pycodex/__init__.py +2 -0
- pycodex/cli.py +101 -30
- pycodex/portable.py +390 -0
- pycodex/portable_server.py +205 -0
- pycodex/runtime.py +6 -2
- pycodex/runtime_services.py +7 -3
- pycodex/tools/exec_tool.py +1 -1
- pycodex/tools/unified_exec_manager.py +19 -2
- pycodex/utils/get_env.py +23 -4
- python_codex-0.1.2.dist-info/METADATA +355 -0
- {python_codex-0.1.0.dist-info → python_codex-0.1.2.dist-info}/RECORD +25 -12
- responses_server/__init__.py +17 -0
- responses_server/__main__.py +5 -0
- responses_server/app.py +217 -0
- responses_server/config.py +63 -0
- responses_server/payload_processors.py +86 -0
- responses_server/server.py +63 -0
- responses_server/session_store.py +37 -0
- responses_server/stream_router.py +784 -0
- responses_server/tools/__init__.py +4 -0
- responses_server/tools/custom_adapter.py +235 -0
- responses_server/tools/web_search.py +263 -0
- python_codex-0.1.0.dist-info/METADATA +0 -267
- {python_codex-0.1.0.dist-info → python_codex-0.1.2.dist-info}/WHEEL +0 -0
- {python_codex-0.1.0.dist-info → python_codex-0.1.2.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.0.dist-info → python_codex-0.1.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-codex
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A minimal Python extraction of Codex's main agent loop
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: cryptography>=3.4
|
|
8
|
+
Requires-Dist: fastapi>=0.115
|
|
9
|
+
Requires-Dist: loguru>=0.7.3
|
|
10
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
11
|
+
Requires-Dist: requests>=2.31
|
|
12
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
13
|
+
Requires-Dist: uvicorn>=0.32
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pycodex
|
|
17
|
+
|
|
18
|
+
English README. Chinese version: `README_ZH.md`
|
|
19
|
+
|
|
20
|
+
PyPI distribution name: `python-codex`
|
|
21
|
+
Import path and CLI command remain `pycodex`.
|
|
22
|
+
|
|
23
|
+
This repository extracts the core Codex agent loop from upstream Codex
|
|
24
|
+
(`https://github.com/openai/codex`) into a deliberately small Python version,
|
|
25
|
+
while preserving the two most important layers:
|
|
26
|
+
|
|
27
|
+
- `submission_loop`: sequentially consumes submitted operations.
|
|
28
|
+
- `run_turn`: keeps executing `model sample -> tool call -> feed tool result
|
|
29
|
+
back into the model` inside a single turn until a final answer is reached.
|
|
30
|
+
|
|
31
|
+
Relevant Rust reference points:
|
|
32
|
+
|
|
33
|
+
- `codex-rs/core/src/codex.rs` -> `submission_loop`
|
|
34
|
+
- `codex-rs/core/src/codex.rs` -> `run_turn`
|
|
35
|
+
- `codex-rs/core/src/codex.rs` -> `run_sampling_request`
|
|
36
|
+
- `codex-rs/core/src/tools/router.rs` -> `ToolRouter`
|
|
37
|
+
- `codex-rs/core/src/stream_events_utils.rs` -> `handle_output_item_done`
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
Install dependencies first:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv sync
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Try the real entry points:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv run pycodex "Reply with exactly OK."
|
|
51
|
+
uv run pycodex
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Design Tradeoffs
|
|
55
|
+
|
|
56
|
+
This is not a 1:1 port of the Rust implementation. The current goal is a
|
|
57
|
+
minimal reusable kernel that converges on the upstream behavior over time:
|
|
58
|
+
|
|
59
|
+
1. Use a thin `ModelClient` protocol to abstract the model side.
|
|
60
|
+
2. Use `ToolRegistry` to manage tool specs and executors.
|
|
61
|
+
3. Use `AgentLoop` to implement the core closed loop.
|
|
62
|
+
4. Use `AgentRuntime` to preserve the outer submission queue so it can keep
|
|
63
|
+
converging toward Rust's `submission_loop` later.
|
|
64
|
+
|
|
65
|
+
Intentionally not included yet:
|
|
66
|
+
|
|
67
|
+
- TUI / streaming incremental rendering
|
|
68
|
+
- MCP / connectors / sandbox / approvals
|
|
69
|
+
- memory / compact / hooks / review mode
|
|
70
|
+
- a full production OpenAI adapter surface
|
|
71
|
+
|
|
72
|
+
All of those can be layered on later. For now, the project is focused on
|
|
73
|
+
nailing the core tool-augmented reasoning loop first.
|
|
74
|
+
|
|
75
|
+
## Layout
|
|
76
|
+
|
|
77
|
+
- `pycodex/protocol.py`: minimal conversation item / prompt / event protocol
|
|
78
|
+
- `pycodex/model.py`: model client protocol and Responses API adapter
|
|
79
|
+
- `pycodex/cli.py`: single-turn and interactive `pycodex` CLI entry points
|
|
80
|
+
- `pycodex/tools/base_tool.py`: `BaseTool`, `ToolRegistry`, `ToolContext`
|
|
81
|
+
- `pycodex/tools/`: concrete tool implementations
|
|
82
|
+
- `pycodex/agent.py`: inner turn loop
|
|
83
|
+
- `pycodex/runtime.py`: outer submission queue
|
|
84
|
+
- `tests/test_agent.py`: core behavior tests
|
|
85
|
+
|
|
86
|
+
## Current Alignment Status
|
|
87
|
+
|
|
88
|
+
Current progress is easiest to read in layers:
|
|
89
|
+
|
|
90
|
+
- prompt/context alignment:
|
|
91
|
+
- on the non-interactive `exec` path, `instructions` and `input` already
|
|
92
|
+
match upstream Codex;
|
|
93
|
+
- this layer is now mainly handled by `pycodex/context.py` plus vendored
|
|
94
|
+
prompt data.
|
|
95
|
+
- turn-loop semantic alignment:
|
|
96
|
+
- `AgentLoop` no longer uses a fixed 12-iteration cap by default;
|
|
97
|
+
- like upstream, it now converges naturally based on whether there is still
|
|
98
|
+
follow-up work or tool handoff to do;
|
|
99
|
+
- the local iteration-limit parameter is gone.
|
|
100
|
+
- request-level alignment:
|
|
101
|
+
- the non-interactive `exec` request body is mostly aligned;
|
|
102
|
+
- the default CLI non-exec first request now also follows the upstream
|
|
103
|
+
`codex-tui` + `<collaboration_mode>` path;
|
|
104
|
+
- the default CLI two-turn main-thread request/header behavior has also been
|
|
105
|
+
captured and aligned, including omitting `workspaces` on later turns;
|
|
106
|
+
- the remaining work is now more about outer behavior branches than this
|
|
107
|
+
already-compared request/header path.
|
|
108
|
+
- tool round-trip alignment:
|
|
109
|
+
- the Default-mode unavailable path for `request_user_input` is aligned to
|
|
110
|
+
real upstream captures;
|
|
111
|
+
- the Plan-mode happy path is also aligned at the tool/protocol layer based
|
|
112
|
+
on upstream source: it forces `isOther=true`, requires non-empty `options`,
|
|
113
|
+
and returns structured answers as a JSON string plus `success=true`;
|
|
114
|
+
- there is now a deterministic round-trip comparison helper,
|
|
115
|
+
`tests/compare_request_user_input_roundtrip.py`, built on the proxy mode in
|
|
116
|
+
`tests/fake_responses_server.py`; against the locally installed
|
|
117
|
+
`codex-cli 0.115.0`, the only remaining Plan-mode live-capture schema
|
|
118
|
+
difference is that `pycodex` includes `success=true` in
|
|
119
|
+
`function_call_output`.
|
|
120
|
+
|
|
121
|
+
See `docs/ALIGNMENT.md` for more detailed notes.
|
|
122
|
+
|
|
123
|
+
## Live Model Integration
|
|
124
|
+
|
|
125
|
+
If this machine already has a Codex CLI configuration, `pycodex` can reuse the
|
|
126
|
+
`model`, `model_provider`, `base_url`, and `env_key` from
|
|
127
|
+
`~/.codex/config.toml` directly:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pycodex import ResponsesModelClient
|
|
131
|
+
|
|
132
|
+
client = ResponsesModelClient.from_codex_config()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The current implementation uses the streaming OpenAI-compatible `/responses`
|
|
136
|
+
endpoint. This path has already been validated against the local
|
|
137
|
+
`~/.codex/config.toml` setup.
|
|
138
|
+
|
|
139
|
+
When launched through the CLI, `pycodex` also loads `.env` from the same
|
|
140
|
+
configuration directory before reading config (typically `~/.codex/.env`), so
|
|
141
|
+
provider keys and similar environment variables can live there. To match
|
|
142
|
+
upstream Codex, variables starting with `CODEX_` are not imported from `.env`.
|
|
143
|
+
|
|
144
|
+
## pycodex CLI
|
|
145
|
+
|
|
146
|
+
`pycodex` now defaults to a minimal interactive entry point. Internally it uses
|
|
147
|
+
`AgentRuntime` to drive the turn submission loop and reuses
|
|
148
|
+
`~/.codex/config.toml` by default:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pycodex
|
|
152
|
+
pycodex "Summarize this repo in one sentence."
|
|
153
|
+
printf 'Reply with exactly OK.' | pycodex
|
|
154
|
+
pycodex --json "Reply with exactly OK."
|
|
155
|
+
pycodex --profile model_proxy "Reply with exactly OK."
|
|
156
|
+
pycodex --vllm-endpoint http://127.0.0.1:18000 "Reply with exactly OK."
|
|
157
|
+
pycodex --put @127.0.0.1:5577
|
|
158
|
+
pycodex --put /data/.codex/@127.0.0.1:5577
|
|
159
|
+
pycodex --call SECRET-CALLID@127.0.0.1:5577 "Reply with exactly OK."
|
|
160
|
+
pycodex doctor
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Current behavior:
|
|
164
|
+
|
|
165
|
+
- with no argv prompt and a TTY stdin, enter interactive mode
|
|
166
|
+
- with an argv prompt or piped stdin, run a single turn
|
|
167
|
+
- interactive mode supports `/exit` and `/quit`
|
|
168
|
+
- interactive mode shows a compact event stream for user-visible phases such as
|
|
169
|
+
tool execution and model follow-up after tool results
|
|
170
|
+
- assistant text is printed from streaming deltas directly
|
|
171
|
+
- interactive mode supports `/history`, `/title`, and `/model`
|
|
172
|
+
- `/model <name>` switches the model used by later turns in the current
|
|
173
|
+
interactive session; `/model` shows the current model and available choices
|
|
174
|
+
- steer is enabled by default in interactive mode: normal input goes into the
|
|
175
|
+
runtime steer path, the current request stops at the next safe boundary, and
|
|
176
|
+
later steer text is appended to the next model request's `input` in order;
|
|
177
|
+
for explicit queueing, use `/queue <message>`, which prints
|
|
178
|
+
`[steer] queued: ...` and later `[steer] inserted: ...`
|
|
179
|
+
- the default built-in tool subset currently exposed as local tools is:
|
|
180
|
+
`shell`, `shell_command`, `exec_command`, `write_stdin`, `exec`, `wait`,
|
|
181
|
+
`web_search`, `update_plan`, `request_user_input`, `request_permissions`,
|
|
182
|
+
`spawn_agent`, `send_input`, `resume_agent`, `wait_agent`, `close_agent`,
|
|
183
|
+
`apply_patch`, `grep_files`, `read_file`, `list_dir`, `view_image`
|
|
184
|
+
- `--vllm-endpoint http://host:port` automatically launches a local
|
|
185
|
+
`responses_server` compatibility layer; when the URL path is empty it is
|
|
186
|
+
normalized to `/v1`, and `/responses` requests are still forwarded to the
|
|
187
|
+
downstream `/v1/chat/completions` endpoint. For `model_provider = "vllm"`,
|
|
188
|
+
reasoning is now preserved across this path: chat chunks with `reasoning` or
|
|
189
|
+
`reasoning_content` are translated back into Responses `reasoning` items, and
|
|
190
|
+
historical `reasoning` items are replayed into downstream assistant messages
|
|
191
|
+
via the `reasoning` field. Streaming token usage is also requested from vLLM
|
|
192
|
+
and forwarded to the final `response.completed.response.usage`
|
|
193
|
+
- `pycodex doctor` checks config, `.env`, API keys, DNS, TCP/TLS, and an
|
|
194
|
+
optional live Responses API request
|
|
195
|
+
|
|
196
|
+
Current primary uses:
|
|
197
|
+
|
|
198
|
+
- verify provider / model / auth configuration
|
|
199
|
+
- debug `ResponsesModelClient`
|
|
200
|
+
- run minimal single-turn and multi-turn smoke tests
|
|
201
|
+
|
|
202
|
+
`doctor` examples:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pycodex doctor
|
|
206
|
+
pycodex doctor --skip-live
|
|
207
|
+
pycodex doctor --json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Portable Mode
|
|
211
|
+
|
|
212
|
+
`Portable Mode` is the quickest way to bring your usual `pycodex` setup into a
|
|
213
|
+
fresh machine, container, or debug image.
|
|
214
|
+
|
|
215
|
+
Use it like this:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pycodex --put @127.0.0.1:5577
|
|
219
|
+
pycodex --put /data/.codex/@127.0.0.1:5577
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- `--put` prints a reusable `SECRET-CALLID@host:port` plus a final one-line
|
|
223
|
+
`pycodex --call ...` command
|
|
224
|
+
- on the new environment or image, run that printed `--call` command directly
|
|
225
|
+
- quickly restoring your usual `config.toml`, `.env`, `AGENTS.md`, and
|
|
226
|
+
`skills/` into a clean debug environment
|
|
227
|
+
- keeping a new image focused on the bug you are debugging instead of spending
|
|
228
|
+
time rebuilding local Codex setup by hand
|
|
229
|
+
- bootstrapping `pycodex` even when the target environment does not already
|
|
230
|
+
have a populated `~/.codex`
|
|
231
|
+
- bare `--put` uses the current user's `~/.codex`
|
|
232
|
+
- `--put /path/.codex/@host:port` lets you publish a different Codex home
|
|
233
|
+
|
|
234
|
+
## Example
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
import asyncio
|
|
238
|
+
|
|
239
|
+
from pycodex import (
|
|
240
|
+
AgentLoop,
|
|
241
|
+
BaseTool,
|
|
242
|
+
ContextManager,
|
|
243
|
+
ResponsesModelClient,
|
|
244
|
+
ToolRegistry,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class EchoTool(BaseTool):
|
|
249
|
+
name = "echo"
|
|
250
|
+
description = "Echo the provided text."
|
|
251
|
+
input_schema = {
|
|
252
|
+
"type": "object",
|
|
253
|
+
"properties": {"text": {"type": "string"}},
|
|
254
|
+
"required": ["text"],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async def run(self, context, args):
|
|
258
|
+
del context
|
|
259
|
+
return args["text"]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def main() -> None:
|
|
263
|
+
model = ResponsesModelClient.from_codex_config()
|
|
264
|
+
context_manager = ContextManager.from_codex_config()
|
|
265
|
+
|
|
266
|
+
tools = ToolRegistry()
|
|
267
|
+
tools.register(EchoTool())
|
|
268
|
+
|
|
269
|
+
agent = AgentLoop(model, tools, context_manager)
|
|
270
|
+
result = await agent.run_turn(
|
|
271
|
+
["Call the echo tool with text=hello, then tell me what it returned."]
|
|
272
|
+
)
|
|
273
|
+
print(result.output_text)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
asyncio.run(main())
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Alignment Checklist
|
|
280
|
+
|
|
281
|
+
See `docs/ALIGNMENT.md` for more detail. This section keeps a high-level
|
|
282
|
+
checklist for quick status scanning.
|
|
283
|
+
|
|
284
|
+
### Tool Alignment
|
|
285
|
+
|
|
286
|
+
Official upstream tools:
|
|
287
|
+
|
|
288
|
+
- [x] `shell` - run shell commands in argv form.
|
|
289
|
+
- [x] `shell_command` - run shell scripts in string form.
|
|
290
|
+
- [x] `exec_command` - start long-running commands with a session.
|
|
291
|
+
- [x] `write_stdin` - write stdin to an existing execution session or poll
|
|
292
|
+
output.
|
|
293
|
+
- [x] `web_search` - expose provider-native web search capability.
|
|
294
|
+
- [x] `update_plan` - update the task plan and maintain step status.
|
|
295
|
+
- [x] `request_user_input` - ask the user structured questions and wait for an
|
|
296
|
+
answer.
|
|
297
|
+
- [x] `request_permissions` - request extra permissions before continuing.
|
|
298
|
+
- [x] `spawn_agent` - create and start a sub-agent.
|
|
299
|
+
- [x] `send_input` - continue feeding input to an existing sub-agent.
|
|
300
|
+
- [x] `resume_agent` - reopen a closed sub-agent.
|
|
301
|
+
- [x] `wait_agent` - wait for a sub-agent to reach a terminal state.
|
|
302
|
+
- [x] `close_agent` - close a sub-agent that is no longer needed.
|
|
303
|
+
- [x] `apply_patch` - edit files precisely with a freeform patch.
|
|
304
|
+
- [x] `grep_files` - search file contents by pattern.
|
|
305
|
+
- [x] `read_file` - read file slices while preserving line-number semantics.
|
|
306
|
+
- [x] `list_dir` - list directory tree slices.
|
|
307
|
+
- [x] `view_image` - turn a local image into model-visible input.
|
|
308
|
+
|
|
309
|
+
Upstream low-frequency / special-mode tools not yet modeled separately:
|
|
310
|
+
|
|
311
|
+
- [ ] `wait_infinite` - long blocking wait for external events or later input.
|
|
312
|
+
- [ ] `spawn_agents_on_csv` - create sub-agent jobs in bulk from CSV.
|
|
313
|
+
- [ ] `report_agent_job_result` - report batch agent job results.
|
|
314
|
+
- [ ] `js_repl` - JavaScript REPL / code-mode primary entry point.
|
|
315
|
+
- [ ] `js_repl_reset` - reset `js_repl` state.
|
|
316
|
+
- [ ] `artifacts` - generate or manage structured artifact outputs.
|
|
317
|
+
- [ ] `list_mcp_resources` - list MCP resources.
|
|
318
|
+
- [ ] `list_mcp_resource_templates` - list MCP resource templates.
|
|
319
|
+
- [ ] `read_mcp_resource` - read MCP resource contents.
|
|
320
|
+
- [ ] `multi_tool_use.parallel` - parallel wrapper around multiple developer
|
|
321
|
+
tool calls.
|
|
322
|
+
|
|
323
|
+
Repository-specific compatibility / transition tools:
|
|
324
|
+
|
|
325
|
+
- [x] `exec` - current local approximation of code mode.
|
|
326
|
+
- [x] `wait` - current local approximation of code-mode waiting behavior.
|
|
327
|
+
|
|
328
|
+
### Behavior Alignment
|
|
329
|
+
|
|
330
|
+
- [x] `AgentLoop` / `AgentRuntime` main loop skeleton - turn loop and submission
|
|
331
|
+
queue are in place.
|
|
332
|
+
- [x] non-interactive `exec` `instructions` alignment - base instructions match
|
|
333
|
+
upstream.
|
|
334
|
+
- [x] non-interactive `exec` `input` alignment - prompt input matches upstream.
|
|
335
|
+
- [x] developer/contextual-user message shape alignment - message/content shape
|
|
336
|
+
matches upstream.
|
|
337
|
+
- [x] `AGENTS.md` + `<environment_context>` injection alignment - context
|
|
338
|
+
assembly order matches upstream.
|
|
339
|
+
- [x] non-interactive `exec` tool subset alignment - the model-visible tool set
|
|
340
|
+
has converged.
|
|
341
|
+
- [x] `include = ["reasoning.encrypted_content"]` - reasoning include field is
|
|
342
|
+
aligned.
|
|
343
|
+
- [x] `prompt_cache_key` - request-level prompt cache key is implemented.
|
|
344
|
+
- [x] `x-client-request-id` - request id header is implemented.
|
|
345
|
+
- [x] `x-codex-turn-metadata` - turn id / sandbox header is implemented.
|
|
346
|
+
- [x] `originator` - mode-aware originator header is implemented.
|
|
347
|
+
- [x] exact `user-agent` string alignment - aligned on the non-interactive
|
|
348
|
+
`exec` path.
|
|
349
|
+
- [x] field-by-field exec-mode tool schema alignment - currently reuses the
|
|
350
|
+
upstream snapshot directly through the tool layer.
|
|
351
|
+
- [ ] full interactive-mode and non-`exec` behavior alignment - the non-exec
|
|
352
|
+
first-turn context is now on the `codex-tui` path, but continuous REPL
|
|
353
|
+
multi-turn behavior is not fully verified yet.
|
|
354
|
+
- [ ] sandbox / approvals / compact / memory and other outer behavior alignment
|
|
355
|
+
- these systems are still in later scope.
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
pycodex/__init__.py,sha256=
|
|
1
|
+
pycodex/__init__.py,sha256=T11JU1QHEk81TchhrTAOqVkvUUiQGlesk9PNaivjPrU,3052
|
|
2
2
|
pycodex/agent.py,sha256=ApIneWSqDxryf9hdmTRFL65AH4e-sn0MWuuR80951Ec,10069
|
|
3
|
-
pycodex/cli.py,sha256=
|
|
3
|
+
pycodex/cli.py,sha256=ju4aF_kwbraqZ-NfoymzB6CjvvMX22afeXDvse2ykf8,24676
|
|
4
4
|
pycodex/collaboration.py,sha256=XAM2enljzHMjzZVlLxbOQF0JhWgKW4qaaDfVcUdE47g,632
|
|
5
5
|
pycodex/context.py,sha256=8-Eg1TE4-GVbEfW0fNZjDWhjLypK3jBlKZY1haYYVPY,23143
|
|
6
6
|
pycodex/doctor.py,sha256=VN-qetM2qJCNRNTZXBMe44VSrEOu8kUXE01luLMF050,10357
|
|
7
7
|
pycodex/model.py,sha256=ZqXSucpzBm0kn2XfhBdKebdwvJQH1Jc9xMqBfPwOKGM,19672
|
|
8
|
+
pycodex/portable.py,sha256=Y2pY08pDiWITY0QYgH3F9YKpOe2EYtxE0qqSmrCkp_g,15260
|
|
9
|
+
pycodex/portable_server.py,sha256=xhEwySCJ41WnsowXM-Db6kkmCOVM02Lmd4pbN6hZzh0,7232
|
|
8
10
|
pycodex/protocol.py,sha256=8mQ7I-y9bxYueSr7d_yGj2Tw69t47OCgwvmxhwihdFw,10807
|
|
9
|
-
pycodex/runtime.py,sha256=
|
|
10
|
-
pycodex/runtime_services.py,sha256=
|
|
11
|
+
pycodex/runtime.py,sha256=tfEuyZmnTP625BQ0NMm-AGhjfQpXcv2EaZLtCJTnEmM,7757
|
|
12
|
+
pycodex/runtime_services.py,sha256=hmdwFiOZ1DPEJ5T8vfDSLfujgGQBPrzPQkn6uX_9vZ8,12503
|
|
11
13
|
pycodex/prompts/collaboration_default.md,sha256=MBTmPuMubeWfZgIeFVj49wwnwD4n_o3fVYAbgWKwu6Q,955
|
|
12
14
|
pycodex/prompts/collaboration_plan.md,sha256=IzjQAA5oHJz-3FmJdOjsJ4LHq6LW1tlEYMoy09n0HKk,8777
|
|
13
15
|
pycodex/prompts/default_base_instructions.md,sha256=D65mcj6bo4CDvVom-D9cbJRJVNquo0NghKt164_fRsg,20923
|
|
@@ -30,7 +32,7 @@ pycodex/tools/close_agent_tool.py,sha256=InKhe2gFWOcqE187J3XYrCckecsyAR48VeVmGdY
|
|
|
30
32
|
pycodex/tools/code_mode_manager.py,sha256=pEczPyCq-3DpJlTtfUEpl4JAGolz8cOpI8mBc7gdrn0,18603
|
|
31
33
|
pycodex/tools/exec_command_tool.py,sha256=_fWfkQLGeINb2-cniY9CWskkAPjC9hE8pfjcBKkWXAg,3459
|
|
32
34
|
pycodex/tools/exec_runtime.js,sha256=ZczdhrzpSZ-qNnJDDJOe8Ap86HpzHb2FZ_vSpHszgLs,3625
|
|
33
|
-
pycodex/tools/exec_tool.py,sha256=
|
|
35
|
+
pycodex/tools/exec_tool.py,sha256=xJfEpcQXpL3OX-ZzKxQ2sI781OuEqpeyPvVkkwhgZ1c,1415
|
|
34
36
|
pycodex/tools/grep_files_tool.py,sha256=twsx1KsvOWh8mi-lbycAtEyh6PeLxtNzl9LzdjwgAf4,4742
|
|
35
37
|
pycodex/tools/list_dir_tool.py,sha256=7S0RsE-NL04G47FmFZtzo-N-O3fPCYQFF0HrjEVuv3U,4749
|
|
36
38
|
pycodex/tools/read_file_tool.py,sha256=GVamhSNEZ1F1IU_og9GgSCzV12TL5t5b1fOUlzTOQBQ,8084
|
|
@@ -41,7 +43,7 @@ pycodex/tools/send_input_tool.py,sha256=z9PR5VoFd9SF4A-ol04Op8AXQF_3YLE74C6coiTX
|
|
|
41
43
|
pycodex/tools/shell_command_tool.py,sha256=Bbah_5HirG1BJOIiqzuMa8kNHNYVPCUvxCFa09eRU6A,3500
|
|
42
44
|
pycodex/tools/shell_tool.py,sha256=BWSaEJZwfQg9Ta-ld2wqeXqavrZC7Y8qgF_vBEOxfYA,3678
|
|
43
45
|
pycodex/tools/spawn_agent_tool.py,sha256=LfJlGI0Ecp9HWNLlTubyybFq-xeRNChILq9ozT7piA8,3556
|
|
44
|
-
pycodex/tools/unified_exec_manager.py,sha256=
|
|
46
|
+
pycodex/tools/unified_exec_manager.py,sha256=ZEaMXmO83Iu20V4dwxAuUjy4EF5IHqHmwnTvFJh8zGc,13330
|
|
45
47
|
pycodex/tools/update_plan_tool.py,sha256=l_EG39bEw5K9BIUKoSUsXYDb0W7aLn8SviKSb-bs7Os,2887
|
|
46
48
|
pycodex/tools/view_image_tool.py,sha256=yB915Jd3he4RjPANdm-dYdvio24OXKhBkAsp-9WVPBg,3924
|
|
47
49
|
pycodex/tools/wait_agent_tool.py,sha256=1tJ5spBtpZ_MjoMv5xmZz5WWKl7UwMqHIJ3SYKXEPZw,2596
|
|
@@ -50,11 +52,22 @@ pycodex/tools/web_search_tool.py,sha256=hq78XF6MRvmNyPFSIp5eI0eYn9ryKdKvvoIOFNU3
|
|
|
50
52
|
pycodex/tools/write_stdin_tool.py,sha256=DghlwPJnAqDoRBYyh1zeXRsfTXoQUdLJ8JQfrdE4RLs,2542
|
|
51
53
|
pycodex/utils/__init__.py,sha256=Hj_0a7RhkAblWkaHyFhpi0cs2nSjJ1NdavbkBgEHieY,1024
|
|
52
54
|
pycodex/utils/dotenv.py,sha256=sOpu6PA1VrsPZK13ynh3nZg3-u9pdiCXkW648v3pwZQ,1789
|
|
53
|
-
pycodex/utils/get_env.py,sha256=
|
|
55
|
+
pycodex/utils/get_env.py,sha256=Ehh0mVPhkDlPMd1WXhrz1UqjPCePg8YfZH7zrtu1EOQ,6894
|
|
54
56
|
pycodex/utils/random_ids.py,sha256=vOEVgkwKeQXaHoEVU7IfsPPjKUABkGIeQ7lu9MZctU8,413
|
|
55
57
|
pycodex/utils/visualize.py,sha256=fK79pTfOwMmRrQujAosGt0nGyyJjpz0GfpWY8BkK91c,35369
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
responses_server/__init__.py,sha256=3yPv_zeGT7P11tTnmj5kXktISLNsNW-02MUnnbiZcb0,394
|
|
59
|
+
responses_server/__main__.py,sha256=9SRp-Yw7ShGxc6DhSIXcDLKgGEdAVm3oBZ59rBOPjT0,62
|
|
60
|
+
responses_server/app.py,sha256=VJSceVaXxbPLu9KGafLIt5fiGvxrqVTkWeOg81O5-FQ,7016
|
|
61
|
+
responses_server/config.py,sha256=XAJmvvLiCYN5jCUcP_6uZyoW79OzxigMbik8X-ZTdKE,2174
|
|
62
|
+
responses_server/payload_processors.py,sha256=_3Sl7HLG00BgN_TKcvT3_3drCDAq1MAeK1HxbRXNta4,3019
|
|
63
|
+
responses_server/server.py,sha256=Q-gjtHzb7K1Guex10G38PgH9hxqJgdzYnBV7Ycy9L7Y,2049
|
|
64
|
+
responses_server/session_store.py,sha256=oP3aFHsGmEMoXuUcxNh6B4vzp6KwaeRdLzM-3AOwM98,1078
|
|
65
|
+
responses_server/stream_router.py,sha256=uYPjrlBUnNqiuQQ91eKmwDfNF6e5NYDtf5C8BdPRPRc,29536
|
|
66
|
+
responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
|
|
67
|
+
responses_server/tools/custom_adapter.py,sha256=ivROeI8D9B1saS7skGLXnwF7fbsjAmEVSbeMceYno4E,8238
|
|
68
|
+
responses_server/tools/web_search.py,sha256=HR9E5uMxWU07khsaIO9zvdg1GCCrWNy73263zfMaxsw,8565
|
|
69
|
+
python_codex-0.1.2.dist-info/METADATA,sha256=wy8bEToZxyf9Cio-upoAS8XmC3MscN8wiKTVXp9MW9g,13969
|
|
70
|
+
python_codex-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
71
|
+
python_codex-0.1.2.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
|
|
72
|
+
python_codex-0.1.2.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
|
|
73
|
+
python_codex-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .app import (
|
|
2
|
+
ManagedResponseServer,
|
|
3
|
+
launch_chat_completion_compat_server,
|
|
4
|
+
run_server,
|
|
5
|
+
)
|
|
6
|
+
from .config import CompatServerConfig
|
|
7
|
+
from .server import ResponseServer
|
|
8
|
+
from .stream_router import StreamRouter
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CompatServerConfig",
|
|
12
|
+
"ManagedResponseServer",
|
|
13
|
+
"ResponseServer",
|
|
14
|
+
"launch_chat_completion_compat_server",
|
|
15
|
+
"run_server",
|
|
16
|
+
"StreamRouter",
|
|
17
|
+
]
|
responses_server/app.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
import json
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Iterator
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI, Request
|
|
12
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
13
|
+
import uvicorn
|
|
14
|
+
|
|
15
|
+
from .config import CompatServerConfig
|
|
16
|
+
from .server import ResponseServer
|
|
17
|
+
from .stream_router import OutcommingChatError, UnsupportedIncommingFeature
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _format_sse_event(event_name: str, payload: dict[str, object]) -> bytes:
|
|
21
|
+
data = json.dumps(payload, ensure_ascii=False)
|
|
22
|
+
return f"event: {event_name}\ndata: {data}\n\n".encode("utf-8")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _stream_events(response_server: ResponseServer, request_body: dict[str, object], request_headers: dict[str, str]) -> Iterator[bytes]:
|
|
26
|
+
try:
|
|
27
|
+
event_iter = response_server.start_response_stream(request_body, request_headers)
|
|
28
|
+
for event_name, payload in event_iter:
|
|
29
|
+
yield _format_sse_event(event_name, payload)
|
|
30
|
+
except OutcommingChatError as exc:
|
|
31
|
+
yield _format_sse_event(
|
|
32
|
+
"response.failed",
|
|
33
|
+
{
|
|
34
|
+
"type": "response.failed",
|
|
35
|
+
"response": {
|
|
36
|
+
"error": {
|
|
37
|
+
"message": str(exc),
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
45
|
+
parser = argparse.ArgumentParser(
|
|
46
|
+
prog="python -m responses_server",
|
|
47
|
+
description=(
|
|
48
|
+
"Standalone localhost `/v1/responses` server that translates the "
|
|
49
|
+
"Codex/Responses subset onto an outcomming `/v1/chat/completions` backend."
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
53
|
+
parser.add_argument("--port", type=int, default=8001)
|
|
54
|
+
parser.add_argument("--outcomming-base-url", required=True)
|
|
55
|
+
parser.add_argument("--outcomming-api-key-env", default=None)
|
|
56
|
+
parser.add_argument("--model-provider", default=None)
|
|
57
|
+
parser.add_argument("--timeout-seconds", type=float, default=120.0)
|
|
58
|
+
return parser
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_server(config: CompatServerConfig) -> None:
|
|
62
|
+
uvicorn.run(
|
|
63
|
+
ManagedResponseServer.build_app(config),
|
|
64
|
+
host=config.host,
|
|
65
|
+
port=config.port,
|
|
66
|
+
log_level="info",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def launch_chat_completion_compat_server(
|
|
71
|
+
base_url: str,
|
|
72
|
+
api_key_env: str | None = None,
|
|
73
|
+
model_provider: str | None = None,
|
|
74
|
+
):
|
|
75
|
+
config = CompatServerConfig.from_base_url(
|
|
76
|
+
base_url,
|
|
77
|
+
api_key_env,
|
|
78
|
+
model_provider=model_provider,
|
|
79
|
+
)
|
|
80
|
+
server = ManagedResponseServer(config)
|
|
81
|
+
server.start()
|
|
82
|
+
return server
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ManagedResponseServer:
|
|
86
|
+
@staticmethod
|
|
87
|
+
def build_app(
|
|
88
|
+
config: CompatServerConfig,
|
|
89
|
+
session_store=None,
|
|
90
|
+
stream_router=None,
|
|
91
|
+
) -> FastAPI:
|
|
92
|
+
response_server = ResponseServer(
|
|
93
|
+
config,
|
|
94
|
+
session_store=session_store,
|
|
95
|
+
stream_router=stream_router,
|
|
96
|
+
)
|
|
97
|
+
app = FastAPI(title="ResponsesCompat", version="0.1.0")
|
|
98
|
+
app.state.response_server = response_server
|
|
99
|
+
|
|
100
|
+
@app.get("/health")
|
|
101
|
+
@app.get("/healthz")
|
|
102
|
+
async def health() -> dict[str, bool]:
|
|
103
|
+
return {"ok": True}
|
|
104
|
+
|
|
105
|
+
@app.get("/models")
|
|
106
|
+
@app.get("/v1/models")
|
|
107
|
+
async def list_models():
|
|
108
|
+
try:
|
|
109
|
+
return response_server.list_models()
|
|
110
|
+
except OutcommingChatError as exc:
|
|
111
|
+
return JSONResponse(
|
|
112
|
+
{"error": {"message": str(exc)}},
|
|
113
|
+
status_code=502,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@app.post("/responses")
|
|
117
|
+
@app.post("/v1/responses")
|
|
118
|
+
async def responses(request: Request):
|
|
119
|
+
try:
|
|
120
|
+
request_body = await request.json()
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
return JSONResponse(
|
|
123
|
+
{"error": {"message": f"invalid JSON body: {exc}"}},
|
|
124
|
+
status_code=400,
|
|
125
|
+
)
|
|
126
|
+
if not isinstance(request_body, dict):
|
|
127
|
+
return JSONResponse(
|
|
128
|
+
{"error": {"message": "request body must be a JSON object"}},
|
|
129
|
+
status_code=400,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
request_headers = {
|
|
133
|
+
str(key).lower(): str(value)
|
|
134
|
+
for key, value in request.headers.items()
|
|
135
|
+
}
|
|
136
|
+
try:
|
|
137
|
+
response_server.stream_router.validate_incomming_request(request_body)
|
|
138
|
+
except UnsupportedIncommingFeature as exc:
|
|
139
|
+
return JSONResponse(
|
|
140
|
+
{"error": {"message": str(exc)}},
|
|
141
|
+
status_code=501,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return StreamingResponse(
|
|
145
|
+
_stream_events(response_server, request_body, request_headers),
|
|
146
|
+
media_type="text/event-stream",
|
|
147
|
+
headers={
|
|
148
|
+
"Cache-Control": "no-cache",
|
|
149
|
+
"Connection": "close",
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return app
|
|
154
|
+
|
|
155
|
+
def __init__(self, config: CompatServerConfig) -> None:
|
|
156
|
+
port = config.port or _reserve_free_port()
|
|
157
|
+
self._config = replace(config, port=port)
|
|
158
|
+
self._app = self.build_app(self._config)
|
|
159
|
+
self._uvicorn_config = uvicorn.Config(
|
|
160
|
+
self._app,
|
|
161
|
+
host=self._config.host,
|
|
162
|
+
port=self._config.port,
|
|
163
|
+
log_level="error",
|
|
164
|
+
access_log=False,
|
|
165
|
+
)
|
|
166
|
+
self._server = uvicorn.Server(self._uvicorn_config)
|
|
167
|
+
self._thread = threading.Thread(target=self._server.run, daemon=True)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def base_url(self) -> str:
|
|
171
|
+
return f"http://{self._config.host}:{self._config.port}/v1"
|
|
172
|
+
|
|
173
|
+
def start(self, timeout_seconds: float = 10.0) -> None:
|
|
174
|
+
self._thread.start()
|
|
175
|
+
deadline = time.time() + timeout_seconds
|
|
176
|
+
while not self._server.started:
|
|
177
|
+
if time.time() >= deadline:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"timed out waiting for managed responses server to start"
|
|
180
|
+
)
|
|
181
|
+
time.sleep(0.01)
|
|
182
|
+
|
|
183
|
+
def stop(self, timeout_seconds: float = 5.0) -> None:
|
|
184
|
+
self._server.should_exit = True
|
|
185
|
+
self._thread.join(timeout=timeout_seconds)
|
|
186
|
+
if self._thread.is_alive():
|
|
187
|
+
raise RuntimeError(
|
|
188
|
+
"timed out waiting for managed responses server to stop"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main() -> None:
|
|
193
|
+
args = build_parser().parse_args()
|
|
194
|
+
run_server(
|
|
195
|
+
CompatServerConfig(
|
|
196
|
+
host=args.host,
|
|
197
|
+
port=args.port,
|
|
198
|
+
outcomming_base_url=args.outcomming_base_url,
|
|
199
|
+
outcomming_api_key_env=args.outcomming_api_key_env,
|
|
200
|
+
model_provider=args.model_provider,
|
|
201
|
+
timeout_seconds=args.timeout_seconds,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
main()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _reserve_free_port() -> int:
|
|
211
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
212
|
+
try:
|
|
213
|
+
sock.bind(("127.0.0.1", 0))
|
|
214
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
215
|
+
return int(sock.getsockname()[1])
|
|
216
|
+
finally:
|
|
217
|
+
sock.close()
|