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.
@@ -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=Cjeqgi3mmkmxE_xcTcPl4heS5k4UGBCG3qFGbwNm7PQ,2986
1
+ pycodex/__init__.py,sha256=T11JU1QHEk81TchhrTAOqVkvUUiQGlesk9PNaivjPrU,3052
2
2
  pycodex/agent.py,sha256=ApIneWSqDxryf9hdmTRFL65AH4e-sn0MWuuR80951Ec,10069
3
- pycodex/cli.py,sha256=d3ug7fR9GeMDDe_BR37RBeUW6w5QnaFRTkGiNgvAkqA,21748
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=Symum2pt6QibV_a3oWm9KWku50u22tLrmXQu76hJSH0,7560
10
- pycodex/runtime_services.py,sha256=Ir2gM7Qf-uSy9JPc8ahL85ifkY5JPdX09y-iHS_qYo8,12374
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=b3_HSlyA0qB9rJulSckLBXOcKS3Lw5xvsQXU-wpNpCs,1414
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=dyuGfaljXTuV4_Bf5Y6OwFGj5_Kb1LW-sh9ni5mMF1Y,12602
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=3l_KA8JCWW9mrKE9FiV2mTx10-e5MUbxaU8jbn3JaRs,6265
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
- python_codex-0.1.0.dist-info/METADATA,sha256=27msS6W8ibM1JsRSE7zMXpVBAvMxdFkBIPlehjwjtAc,11720
57
- python_codex-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
58
- python_codex-0.1.0.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
59
- python_codex-0.1.0.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
60
- python_codex-0.1.0.dist-info/RECORD,,
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
+ ]
@@ -0,0 +1,5 @@
1
+ from .app import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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()