python-codex 0.1.2__py3-none-any.whl → 0.1.4__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 +5 -1
- pycodex/agent.py +89 -51
- pycodex/cli.py +152 -45
- pycodex/collaboration.py +6 -7
- pycodex/compat.py +99 -0
- pycodex/context.py +110 -87
- pycodex/doctor.py +40 -40
- pycodex/model.py +429 -90
- pycodex/portable.py +33 -33
- pycodex/portable_server.py +22 -21
- pycodex/prompts/models.json +30 -0
- pycodex/protocol.py +84 -86
- pycodex/runtime.py +36 -35
- pycodex/runtime_services.py +69 -69
- pycodex/tools/agent_tool_schemas.py +0 -2
- pycodex/tools/apply_patch_tool.py +45 -46
- pycodex/tools/base_tool.py +35 -36
- pycodex/tools/close_agent_tool.py +2 -4
- pycodex/tools/code_mode_manager.py +61 -61
- pycodex/tools/exec_command_tool.py +5 -6
- pycodex/tools/exec_runtime.js +3 -3
- pycodex/tools/exec_tool.py +2 -4
- pycodex/tools/grep_files_tool.py +10 -11
- pycodex/tools/list_dir_tool.py +8 -9
- pycodex/tools/read_file_tool.py +13 -14
- pycodex/tools/request_permissions_tool.py +2 -4
- pycodex/tools/request_user_input_tool.py +13 -14
- pycodex/tools/resume_agent_tool.py +2 -4
- pycodex/tools/send_input_tool.py +8 -9
- pycodex/tools/shell_command_tool.py +5 -6
- pycodex/tools/shell_tool.py +5 -6
- pycodex/tools/spawn_agent_tool.py +4 -5
- pycodex/tools/unified_exec_manager.py +62 -61
- pycodex/tools/update_plan_tool.py +4 -5
- pycodex/tools/view_image_tool.py +4 -5
- pycodex/tools/wait_agent_tool.py +2 -4
- pycodex/tools/wait_tool.py +4 -5
- pycodex/tools/web_search_tool.py +1 -3
- pycodex/tools/write_stdin_tool.py +4 -5
- pycodex/utils/__init__.py +4 -0
- pycodex/utils/compactor.py +189 -0
- pycodex/utils/dotenv.py +6 -6
- pycodex/utils/get_env.py +37 -33
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +483 -0
- pycodex/utils/visualize.py +197 -83
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/METADATA +32 -11
- python_codex-0.1.4.dist-info/RECORD +76 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/WHEEL +1 -1
- responses_server/app.py +32 -20
- responses_server/config.py +17 -17
- responses_server/payload_processors.py +26 -17
- responses_server/server.py +11 -11
- responses_server/session_store.py +10 -10
- responses_server/stream_router.py +83 -64
- responses_server/tools/custom_adapter.py +12 -12
- responses_server/tools/web_search.py +33 -33
- python_codex-0.1.2.dist-info/RECORD +0 -73
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: python-codex
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A minimal Python extraction of Codex's main agent loop
|
|
5
5
|
License-File: LICENSE
|
|
6
|
-
Requires-Python: >=3.
|
|
7
|
-
Requires-Dist: cryptography
|
|
8
|
-
Requires-Dist:
|
|
6
|
+
Requires-Python: >=3.6.2
|
|
7
|
+
Requires-Dist: cryptography<41,>=40.0.2; python_version < '3.7'
|
|
8
|
+
Requires-Dist: cryptography>=40.0.2; python_version >= '3.7'
|
|
9
|
+
Requires-Dist: dataclasses>=0.8; python_version < '3.7'
|
|
10
|
+
Requires-Dist: fastapi<0.84,>=0.83.0; python_version < '3.7'
|
|
11
|
+
Requires-Dist: fastapi>=0.83.0; python_version >= '3.7'
|
|
12
|
+
Requires-Dist: importlib-metadata>=4.8.3; python_version < '3.8'
|
|
9
13
|
Requires-Dist: loguru>=0.7.3
|
|
10
|
-
Requires-Dist: prompt-toolkit>=3.0
|
|
11
|
-
Requires-Dist: requests>=2.
|
|
12
|
-
Requires-Dist: tomli
|
|
13
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: prompt-toolkit>=3.0.36
|
|
15
|
+
Requires-Dist: requests>=2.27.1
|
|
16
|
+
Requires-Dist: tomli<2,>=1.2.3; python_version < '3.11'
|
|
17
|
+
Requires-Dist: typing-extensions>=4.1.1; python_version < '3.8'
|
|
18
|
+
Requires-Dist: uvicorn<0.17,>=0.16.0; python_version < '3.7'
|
|
19
|
+
Requires-Dist: uvicorn>=0.16.0; python_version >= '3.7'
|
|
14
20
|
Description-Content-Type: text/markdown
|
|
15
21
|
|
|
16
22
|
# pycodex
|
|
@@ -66,7 +72,7 @@ Intentionally not included yet:
|
|
|
66
72
|
|
|
67
73
|
- TUI / streaming incremental rendering
|
|
68
74
|
- MCP / connectors / sandbox / approvals
|
|
69
|
-
- memory / compact /
|
|
75
|
+
- memory / compact / review mode
|
|
70
76
|
- a full production OpenAI adapter surface
|
|
71
77
|
|
|
72
78
|
All of those can be layered on later. For now, the project is focused on
|
|
@@ -168,9 +174,24 @@ Current behavior:
|
|
|
168
174
|
- interactive mode shows a compact event stream for user-visible phases such as
|
|
169
175
|
tool execution and model follow-up after tool results
|
|
170
176
|
- assistant text is printed from streaming deltas directly
|
|
171
|
-
- interactive mode supports `/history`, `/title`, and `/
|
|
177
|
+
- interactive mode supports `/history`, `/title`, `/model`, `/resume`, and `/compact`
|
|
172
178
|
- `/model <name>` switches the model used by later turns in the current
|
|
173
179
|
interactive session; `/model` shows the current model and available choices
|
|
180
|
+
- `/resume` with no argument lists the currently resumable sessions by their
|
|
181
|
+
first user-message preview; `/resume 1` resumes the first listed session
|
|
182
|
+
- `/resume <number>` replaces the in-memory history with the selected recorded
|
|
183
|
+
Codex rollout from `CODEX_HOME/sessions`
|
|
184
|
+
- `/compact` synthesizes a local handoff summary, replaces the in-memory
|
|
185
|
+
conversation history with the compacted view, and appends a compacted-history
|
|
186
|
+
entry to the rollout so later `/resume` sees the same state
|
|
187
|
+
- new sessions are now recorded under `CODEX_HOME/sessions/.../rollout-*.jsonl`
|
|
188
|
+
with a stable session/thread id and per-item append+flush semantics so
|
|
189
|
+
`/resume` reads back the same rollout format
|
|
190
|
+
- if `TURN_HOOK.md` exists in the workspace root and is non-empty, each
|
|
191
|
+
completed turn also forks the just-finished history into a temporary,
|
|
192
|
+
non-persisted follow-up session and submits the file contents as the next
|
|
193
|
+
user instruction; this is intended for side-effect follow-ups such as
|
|
194
|
+
Feishu notifications
|
|
174
195
|
- steer is enabled by default in interactive mode: normal input goes into the
|
|
175
196
|
runtime steer path, the current request stops at the next safe boundary, and
|
|
176
197
|
later steer text is appended to the next model request's `input` in order;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
pycodex/__init__.py,sha256=jCnC_Bgotlxa4GwO3Re2sChKGY49TRM-uVZEQ9uBpfw,3106
|
|
2
|
+
pycodex/agent.py,sha256=s0FrF_XG2pHKryooS461Jr_acmQ_TKTp2JLGQNiny6w,11888
|
|
3
|
+
pycodex/cli.py,sha256=ntgC0LWlSOhuYAUOBgSEeVIjBTKS91klyvkTO9QtFoE,29559
|
|
4
|
+
pycodex/collaboration.py,sha256=yQ6pBD-R3ZWR4_FAYQFoS7KF0m4LLD42otXIbPqw2ys,641
|
|
5
|
+
pycodex/compat.py,sha256=IO0X7AgcYhlHnYnpvBZ6leCh_UjoQzg5HLT5wYBNNIw,3155
|
|
6
|
+
pycodex/context.py,sha256=R5tuMcNrX1F-Lh9ymsSbnfRbKLJ19TWrtQoZ3tWlHvM,24982
|
|
7
|
+
pycodex/doctor.py,sha256=De3M4hRBJq8ZeqsUJgHz0vitqrH18YugrEnz7oHhTdQ,10572
|
|
8
|
+
pycodex/model.py,sha256=Mk9LZKmFcXG71I18-gs4dUWNn0GIM1rbMhFfKDut_3w,32790
|
|
9
|
+
pycodex/portable.py,sha256=kZ5XVOMZq0l6xXsx3FY9C3DfB4Jra5Hw38qTMH0TEwg,15597
|
|
10
|
+
pycodex/portable_server.py,sha256=6I3pQkWj3e_SFlDXY2mGdCPns1w_3PSxByBV9wv5epI,7331
|
|
11
|
+
pycodex/protocol.py,sha256=LYDzJefu1tugqQzee4NuZzxhGAv3hXrNcnlw04CudAY,11106
|
|
12
|
+
pycodex/runtime.py,sha256=gpDDxQKfp1Cqh1U0uslI3rCoXEN7XpJGuHlV-bsveM0,7983
|
|
13
|
+
pycodex/runtime_services.py,sha256=OJjBWMjLAoc1cxLPKmz03XhMN5jOiBBNhxiKROytIbc,13118
|
|
14
|
+
pycodex/prompts/collaboration_default.md,sha256=MBTmPuMubeWfZgIeFVj49wwnwD4n_o3fVYAbgWKwu6Q,955
|
|
15
|
+
pycodex/prompts/collaboration_plan.md,sha256=IzjQAA5oHJz-3FmJdOjsJ4LHq6LW1tlEYMoy09n0HKk,8777
|
|
16
|
+
pycodex/prompts/default_base_instructions.md,sha256=D65mcj6bo4CDvVom-D9cbJRJVNquo0NghKt164_fRsg,20923
|
|
17
|
+
pycodex/prompts/exec_tools.json,sha256=2wYLsjL6VGzMnhFNCxE9IA_kxsxUspN68lr7JOlZq54,23369
|
|
18
|
+
pycodex/prompts/models.json,sha256=u4u2bylNZnMw_qKtvn_iZMUwS4wEq1JBMwHcetC3Spo,285814
|
|
19
|
+
pycodex/prompts/subagent_tools.json,sha256=2ZOXyAiAaai2aazIlXdjjXb7cra5gZ2WYYbPltPaiYg,6199
|
|
20
|
+
pycodex/prompts/permissions/approval_policy/never.md,sha256=QceTG6wjkaJARjYr0HYV1aPnPcpGcrkRUW-smWRr6MQ,120
|
|
21
|
+
pycodex/prompts/permissions/approval_policy/on_failure.md,sha256=dfJjpXkpO6_ANdCKxbVJ8o4vyLxevrJWfKsGHTqtbkc,289
|
|
22
|
+
pycodex/prompts/permissions/approval_policy/on_request.md,sha256=hVQalzh0FAdkKzw5u-N4H7-LtC9ijVDlYsh3OKsZKzo,3661
|
|
23
|
+
pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md,sha256=mOinishp1k-wlPsaEuIOMn5GoVm_dAIsWIuEMmv2r7o,1725
|
|
24
|
+
pycodex/prompts/permissions/approval_policy/unless_trusted.md,sha256=XHpi1Lfx1iIXFbbQ_ho_kGstA3JN-RLho291HM30UNw,247
|
|
25
|
+
pycodex/prompts/permissions/sandbox_mode/danger_full_access.md,sha256=nZ7YHacBd3cAHKRZc9XClOOOnXJPXPh0WFBueh5C2D0,197
|
|
26
|
+
pycodex/prompts/permissions/sandbox_mode/read_only.md,sha256=2rAPEXsBYCcuttI5j3euS-3uv_v97catIsnhxlSQSIM,173
|
|
27
|
+
pycodex/prompts/permissions/sandbox_mode/workspace_write.md,sha256=lVN-LwrBbHqlv5yVjcd_mU8tzZW8jfKpTatJKIZu9HI,277
|
|
28
|
+
pycodex/tools/__init__.py,sha256=aSLXrr_31KGQgDfRow5zVIc-2-KdXlHaCE6qUnE4HWI,1772
|
|
29
|
+
pycodex/tools/agent_tool_schemas.py,sha256=r7pBICcx8fb0Rg6IzIg8-u3um2z11TogQ4yCzuiO-4o,2033
|
|
30
|
+
pycodex/tools/apply_patch_tool.py,sha256=aFob-gzaCXlzPdCIvRXVKm1NrQqqhqe8CVkFVAhqiTc,13955
|
|
31
|
+
pycodex/tools/base_tool.py,sha256=FLtbb6KPUKyhHRMrR6_anYi_GmpJFCaX1ch5aRnjQjo,5527
|
|
32
|
+
pycodex/tools/close_agent_tool.py,sha256=nY3l_UOX6NyTgUqdXag3yRpdyQScV0g0Vv4HE3ElLwg,1597
|
|
33
|
+
pycodex/tools/code_mode_manager.py,sha256=Wow42H_9IomUKUjjjU8rrAFAklhE-UlgxgrbgHRU_4M,19031
|
|
34
|
+
pycodex/tools/exec_command_tool.py,sha256=l8GWlZKTvlWWAd_OPKsnnt3m0woMWXK8NkilmspnaQQ,3485
|
|
35
|
+
pycodex/tools/exec_runtime.js,sha256=DR1uocKailTqNWAcJNFJuQgFFMSUzTpT_uQsRaneg2s,3643
|
|
36
|
+
pycodex/tools/exec_tool.py,sha256=pwzFCyjQMw2O9A6JDSTyOz6wmwg-UkFh_4wTY9rPLTI,1389
|
|
37
|
+
pycodex/tools/grep_files_tool.py,sha256=OiMKpM9vfaTpfEllQaI6Td39NnQ0gRDXqxMfmBwV2yQ,4797
|
|
38
|
+
pycodex/tools/list_dir_tool.py,sha256=qX4AGkmKkEt7qJaYTsaGIB0zCiyzcU5U8IBapAjEIQ0,4792
|
|
39
|
+
pycodex/tools/read_file_tool.py,sha256=xJk4f-lI-CJm50d5EThzWCz-7l2IX8s2gVwp-evsAJE,8148
|
|
40
|
+
pycodex/tools/request_permissions_tool.py,sha256=n0V0WvpaW3HBnBbdQoyi9CfL1dRBJDn7t9sWpg8wZ4g,3010
|
|
41
|
+
pycodex/tools/request_user_input_tool.py,sha256=mm3MbFIj6PYGgX4dv8MKSFCuiEFvHp23YGoJM2jv1ks,5734
|
|
42
|
+
pycodex/tools/resume_agent_tool.py,sha256=o62xdrsRxGFdRLxEhKEny-YEcaBOeqIneImrkME35II,1614
|
|
43
|
+
pycodex/tools/send_input_tool.py,sha256=P4VvZlNcHbfjhWF4vfEyHwDpyeyrhYJWLvvlWfvub2M,3608
|
|
44
|
+
pycodex/tools/shell_command_tool.py,sha256=hC2sg_ishmrqFIVyY5ngKnMOXjjIDL3xQ-gg40R1OQI,3520
|
|
45
|
+
pycodex/tools/shell_tool.py,sha256=7WaCWJM_-VeqyL5do_Qc8D-OZFWMXXY5Eo-ApSSsO9U,3698
|
|
46
|
+
pycodex/tools/spawn_agent_tool.py,sha256=YO5RLZpgv6uQPaEiCnOIGGBmGWREknUavvyAOFWWWeE,3563
|
|
47
|
+
pycodex/tools/unified_exec_manager.py,sha256=dSKv5fTSTWlSTktWyUXshGVijH9J74iuCYQxuse0oCY,13699
|
|
48
|
+
pycodex/tools/update_plan_tool.py,sha256=CiTTGAtG2b0EJyYsm2MgN_GXEKvCN8fvTgJGAcUCu-M,2884
|
|
49
|
+
pycodex/tools/view_image_tool.py,sha256=s3P1wPZioKrjjNXbwYEz9pzfVsnlPY79LzL-vujqRfY,3940
|
|
50
|
+
pycodex/tools/wait_agent_tool.py,sha256=0xjr5M2S0SNZaSr1o4U0RXI6dTJfMVpBB8Uclm_402I,2570
|
|
51
|
+
pycodex/tools/wait_tool.py,sha256=EJcW2Ev9jUD9eZ7cFDNOLDzlywS2BD3ll6pArXyxfrI,2331
|
|
52
|
+
pycodex/tools/web_search_tool.py,sha256=_7r2ltWhnBM0ZCgweA5a0GbEi0qSFAHOyi1RHrl6tfQ,957
|
|
53
|
+
pycodex/tools/write_stdin_tool.py,sha256=nCuProkbeewfQ_yS8CgBajo--K3EmkXzJYh1D2QtAM4,2549
|
|
54
|
+
pycodex/utils/__init__.py,sha256=XawMC7CRm9bt3wPWyithj5x7YQvYrggn2_DcGGSTnCY,1162
|
|
55
|
+
pycodex/utils/compactor.py,sha256=ZCzGc02xHmXq1rIjnG2gATKcFtt6r-OGsCIK0ypjnyI,6467
|
|
56
|
+
pycodex/utils/dotenv.py,sha256=EDBXdn93ewmq9zhJki5_LsJJXe0wMIQJ6VfCE1r7voQ,1818
|
|
57
|
+
pycodex/utils/get_env.py,sha256=jR8G0Xco57jX-71E1oHIcl3-Kz9Ltc0kzxj04DKzt80,7316
|
|
58
|
+
pycodex/utils/random_ids.py,sha256=zBphjVGc7OXk9ZNExAbxRi_bk7ipyLG491qTv7hi8jM,380
|
|
59
|
+
pycodex/utils/session_persist.py,sha256=dUvo3Z1QBB4HJT1tLerDlLD3ZB25umB6FP6JORg9V40,16414
|
|
60
|
+
pycodex/utils/visualize.py,sha256=9S3oOUAnI_SbVvoFJ18dzq8MLE5v6kAsNiYsMTtqKAc,40022
|
|
61
|
+
responses_server/__init__.py,sha256=3yPv_zeGT7P11tTnmj5kXktISLNsNW-02MUnnbiZcb0,394
|
|
62
|
+
responses_server/__main__.py,sha256=9SRp-Yw7ShGxc6DhSIXcDLKgGEdAVm3oBZ59rBOPjT0,62
|
|
63
|
+
responses_server/app.py,sha256=AtysZYL6ViheHYISS8eCK_iyr7CwUfF3wrt86ekh79U,7371
|
|
64
|
+
responses_server/config.py,sha256=wEcZbXZclTYz4fI_oy_sSMglWPeEITWlFeglQrrr6HE,2236
|
|
65
|
+
responses_server/payload_processors.py,sha256=AcOipqVQyo4wKw_pb3ABlarwIK1VjcnQTlgPehRVGO8,3412
|
|
66
|
+
responses_server/server.py,sha256=isyzN-p-Ir8LLycN_dQfcanvie2ZqqSu52mOPz_wYD4,2095
|
|
67
|
+
responses_server/session_store.py,sha256=ZD3cH2aEOkWaQsu5qTzcal2mThTSFQPAhAhPUN9srgI,1115
|
|
68
|
+
responses_server/stream_router.py,sha256=zWC4yyZ3I8E-Zgco844tIhRMWOwIkjOV0s-G-a9-B8k,30861
|
|
69
|
+
responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
|
|
70
|
+
responses_server/tools/custom_adapter.py,sha256=LxO7ldydvR-GWachDz8GKC0Q8KGGFoFPbZxM0QvxuZ0,8350
|
|
71
|
+
responses_server/tools/web_search.py,sha256=pm4ZUiHUfxc0bGY1kEvt-BCzDrZIyP24xzPUcga2ul0,8908
|
|
72
|
+
python_codex-0.1.4.dist-info/METADATA,sha256=fSNjm5GPh613W0ZFzU3UJFatqKUUs0xWYW17aOY4eLg,15451
|
|
73
|
+
python_codex-0.1.4.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
|
|
74
|
+
python_codex-0.1.4.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
|
|
75
|
+
python_codex-0.1.4.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
|
|
76
|
+
python_codex-0.1.4.dist-info/RECORD,,
|
responses_server/app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import argparse
|
|
3
|
+
import asyncio
|
|
4
4
|
from dataclasses import replace
|
|
5
5
|
import json
|
|
6
6
|
import socket
|
|
@@ -15,33 +15,41 @@ import uvicorn
|
|
|
15
15
|
from .config import CompatServerConfig
|
|
16
16
|
from .server import ResponseServer
|
|
17
17
|
from .stream_router import OutcommingChatError, UnsupportedIncommingFeature
|
|
18
|
+
import typing
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def
|
|
21
|
+
def _run_uvicorn_server(server) -> 'None':
|
|
22
|
+
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
23
|
+
server.run()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _format_sse_event(event_name: 'str', payload: 'typing.Dict[str, object]') -> 'bytes':
|
|
21
27
|
data = json.dumps(payload, ensure_ascii=False)
|
|
22
28
|
return f"event: {event_name}\ndata: {data}\n\n".encode("utf-8")
|
|
23
29
|
|
|
24
30
|
|
|
25
|
-
def _stream_events(response_server: ResponseServer, request_body:
|
|
31
|
+
def _stream_events(response_server: 'ResponseServer', request_body: 'typing.Dict[str, object]', request_headers: 'typing.Dict[str, str]') -> 'Iterator[bytes]':
|
|
26
32
|
try:
|
|
27
33
|
event_iter = response_server.start_response_stream(request_body, request_headers)
|
|
28
34
|
for event_name, payload in event_iter:
|
|
29
35
|
yield _format_sse_event(event_name, payload)
|
|
30
36
|
except OutcommingChatError as exc:
|
|
37
|
+
|
|
38
|
+
import traceback
|
|
31
39
|
yield _format_sse_event(
|
|
32
40
|
"response.failed",
|
|
33
41
|
{
|
|
34
42
|
"type": "response.failed",
|
|
35
43
|
"response": {
|
|
36
44
|
"error": {
|
|
37
|
-
"message":
|
|
45
|
+
"message": '\n'.join(traceback.format_exception(exc)),
|
|
38
46
|
}
|
|
39
47
|
},
|
|
40
48
|
},
|
|
41
49
|
)
|
|
42
50
|
|
|
43
51
|
|
|
44
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
52
|
+
def build_parser() -> 'argparse.ArgumentParser':
|
|
45
53
|
parser = argparse.ArgumentParser(
|
|
46
54
|
prog="python -m responses_server",
|
|
47
55
|
description=(
|
|
@@ -58,7 +66,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
58
66
|
return parser
|
|
59
67
|
|
|
60
68
|
|
|
61
|
-
def run_server(config: CompatServerConfig) -> None:
|
|
69
|
+
def run_server(config: 'CompatServerConfig') -> 'None':
|
|
62
70
|
uvicorn.run(
|
|
63
71
|
ManagedResponseServer.build_app(config),
|
|
64
72
|
host=config.host,
|
|
@@ -68,9 +76,9 @@ def run_server(config: CompatServerConfig) -> None:
|
|
|
68
76
|
|
|
69
77
|
|
|
70
78
|
def launch_chat_completion_compat_server(
|
|
71
|
-
base_url: str,
|
|
72
|
-
api_key_env: str
|
|
73
|
-
model_provider: str
|
|
79
|
+
base_url: 'str',
|
|
80
|
+
api_key_env: 'typing.Union[str, None]' = None,
|
|
81
|
+
model_provider: 'typing.Union[str, None]' = None,
|
|
74
82
|
):
|
|
75
83
|
config = CompatServerConfig.from_base_url(
|
|
76
84
|
base_url,
|
|
@@ -85,10 +93,10 @@ def launch_chat_completion_compat_server(
|
|
|
85
93
|
class ManagedResponseServer:
|
|
86
94
|
@staticmethod
|
|
87
95
|
def build_app(
|
|
88
|
-
config: CompatServerConfig,
|
|
96
|
+
config: 'CompatServerConfig',
|
|
89
97
|
session_store=None,
|
|
90
98
|
stream_router=None,
|
|
91
|
-
) -> FastAPI:
|
|
99
|
+
) -> 'FastAPI':
|
|
92
100
|
response_server = ResponseServer(
|
|
93
101
|
config,
|
|
94
102
|
session_store=session_store,
|
|
@@ -99,7 +107,7 @@ class ManagedResponseServer:
|
|
|
99
107
|
|
|
100
108
|
@app.get("/health")
|
|
101
109
|
@app.get("/healthz")
|
|
102
|
-
async def health() ->
|
|
110
|
+
async def health() -> 'typing.Dict[str, bool]':
|
|
103
111
|
return {"ok": True}
|
|
104
112
|
|
|
105
113
|
@app.get("/models")
|
|
@@ -115,7 +123,7 @@ class ManagedResponseServer:
|
|
|
115
123
|
|
|
116
124
|
@app.post("/responses")
|
|
117
125
|
@app.post("/v1/responses")
|
|
118
|
-
async def responses(request: Request):
|
|
126
|
+
async def responses(request: 'Request'):
|
|
119
127
|
try:
|
|
120
128
|
request_body = await request.json()
|
|
121
129
|
except Exception as exc:
|
|
@@ -152,7 +160,7 @@ class ManagedResponseServer:
|
|
|
152
160
|
|
|
153
161
|
return app
|
|
154
162
|
|
|
155
|
-
def __init__(self, config: CompatServerConfig) -> None:
|
|
163
|
+
def __init__(self, config: 'CompatServerConfig') -> 'None':
|
|
156
164
|
port = config.port or _reserve_free_port()
|
|
157
165
|
self._config = replace(config, port=port)
|
|
158
166
|
self._app = self.build_app(self._config)
|
|
@@ -164,13 +172,17 @@ class ManagedResponseServer:
|
|
|
164
172
|
access_log=False,
|
|
165
173
|
)
|
|
166
174
|
self._server = uvicorn.Server(self._uvicorn_config)
|
|
167
|
-
self._thread = threading.Thread(
|
|
175
|
+
self._thread = threading.Thread(
|
|
176
|
+
target=_run_uvicorn_server,
|
|
177
|
+
args=(self._server,),
|
|
178
|
+
daemon=True,
|
|
179
|
+
)
|
|
168
180
|
|
|
169
181
|
@property
|
|
170
|
-
def base_url(self) -> str:
|
|
182
|
+
def base_url(self) -> 'str':
|
|
171
183
|
return f"http://{self._config.host}:{self._config.port}/v1"
|
|
172
184
|
|
|
173
|
-
def start(self, timeout_seconds: float = 10.0) -> None:
|
|
185
|
+
def start(self, timeout_seconds: 'float' = 10.0) -> 'None':
|
|
174
186
|
self._thread.start()
|
|
175
187
|
deadline = time.time() + timeout_seconds
|
|
176
188
|
while not self._server.started:
|
|
@@ -180,7 +192,7 @@ class ManagedResponseServer:
|
|
|
180
192
|
)
|
|
181
193
|
time.sleep(0.01)
|
|
182
194
|
|
|
183
|
-
def stop(self, timeout_seconds: float = 5.0) -> None:
|
|
195
|
+
def stop(self, timeout_seconds: 'float' = 5.0) -> 'None':
|
|
184
196
|
self._server.should_exit = True
|
|
185
197
|
self._thread.join(timeout=timeout_seconds)
|
|
186
198
|
if self._thread.is_alive():
|
|
@@ -189,7 +201,7 @@ class ManagedResponseServer:
|
|
|
189
201
|
)
|
|
190
202
|
|
|
191
203
|
|
|
192
|
-
def main() -> None:
|
|
204
|
+
def main() -> 'None':
|
|
193
205
|
args = build_parser().parse_args()
|
|
194
206
|
run_server(
|
|
195
207
|
CompatServerConfig(
|
|
@@ -207,7 +219,7 @@ if __name__ == "__main__":
|
|
|
207
219
|
main()
|
|
208
220
|
|
|
209
221
|
|
|
210
|
-
def _reserve_free_port() -> int:
|
|
222
|
+
def _reserve_free_port() -> 'int':
|
|
211
223
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
212
224
|
try:
|
|
213
225
|
sock.bind(("127.0.0.1", 0))
|
responses_server/config.py
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import os
|
|
4
3
|
from dataclasses import dataclass
|
|
5
4
|
import urllib.parse
|
|
5
|
+
import typing
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
@dataclass(frozen=True,
|
|
8
|
+
@dataclass(frozen=True, )
|
|
9
9
|
class CompatServerConfig:
|
|
10
|
-
host: str = "127.0.0.1"
|
|
11
|
-
port: int = 0
|
|
12
|
-
outcomming_base_url: str = "http://127.0.0.1:8000/v1"
|
|
13
|
-
outcomming_api_key_env: str
|
|
14
|
-
model_provider: str
|
|
15
|
-
timeout_seconds: float = 120.0
|
|
16
|
-
|
|
17
|
-
def outcomming_api_key(self) -> str
|
|
10
|
+
host: 'str' = "127.0.0.1"
|
|
11
|
+
port: 'int' = 0
|
|
12
|
+
outcomming_base_url: 'str' = "http://127.0.0.1:8000/v1"
|
|
13
|
+
outcomming_api_key_env: 'typing.Union[str, None]' = None
|
|
14
|
+
model_provider: 'typing.Union[str, None]' = None
|
|
15
|
+
timeout_seconds: 'float' = 120.0
|
|
16
|
+
|
|
17
|
+
def outcomming_api_key(self) -> 'typing.Union[str, None]':
|
|
18
18
|
if self.outcomming_api_key_env is None:
|
|
19
19
|
return None
|
|
20
20
|
value = os.environ.get(self.outcomming_api_key_env, "").strip()
|
|
21
21
|
return value or None
|
|
22
22
|
|
|
23
|
-
def outcomming_chat_completions_url(self) -> str:
|
|
23
|
+
def outcomming_chat_completions_url(self) -> 'str':
|
|
24
24
|
base = self.outcomming_base_url.rstrip("/")
|
|
25
25
|
return f"{base}/chat/completions"
|
|
26
26
|
|
|
27
|
-
def outcomming_models_url(self) -> str:
|
|
27
|
+
def outcomming_models_url(self) -> 'str':
|
|
28
28
|
base = self.outcomming_base_url.rstrip("/")
|
|
29
29
|
return f"{base}/models"
|
|
30
30
|
|
|
31
|
-
def with_ephemeral_port(self) -> CompatServerConfig:
|
|
31
|
+
def with_ephemeral_port(self) -> 'CompatServerConfig':
|
|
32
32
|
return CompatServerConfig(
|
|
33
33
|
host=self.host,
|
|
34
34
|
port=0,
|
|
@@ -41,10 +41,10 @@ class CompatServerConfig:
|
|
|
41
41
|
@classmethod
|
|
42
42
|
def from_base_url(
|
|
43
43
|
cls,
|
|
44
|
-
outcomming_base_url: str,
|
|
45
|
-
api_key_env: str
|
|
46
|
-
model_provider: str
|
|
47
|
-
) -> CompatServerConfig:
|
|
44
|
+
outcomming_base_url: 'str',
|
|
45
|
+
api_key_env: 'typing.Union[str, None]' = None,
|
|
46
|
+
model_provider: 'typing.Union[str, None]' = None,
|
|
47
|
+
) -> 'CompatServerConfig':
|
|
48
48
|
parsed = urllib.parse.urlparse(outcomming_base_url)
|
|
49
49
|
if not parsed.scheme or not parsed.netloc:
|
|
50
50
|
raise ValueError(f"invalid outcomming base url: {outcomming_base_url}")
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
"""Provider-specific post-process hooks for canonical outgoing chat requests.
|
|
4
3
|
|
|
@@ -9,11 +8,12 @@ building one canonical `outcomming_request`, while `server.py` selects the
|
|
|
9
8
|
appropriate hook from `CompatServerConfig.model_provider`.
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
|
-
from collections.abc import Callable
|
|
13
11
|
from copy import deepcopy
|
|
14
|
-
from typing import
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
import typing
|
|
14
|
+
from typing_extensions import TypedDict
|
|
15
15
|
|
|
16
|
-
ChatMessage =
|
|
16
|
+
ChatMessage = typing.Dict[str, object]
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class OutgoingRequest(TypedDict):
|
|
@@ -25,24 +25,24 @@ class OutgoingRequest(TypedDict):
|
|
|
25
25
|
not rely on TypedDict inheritance.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
model: str
|
|
29
|
-
messages:
|
|
30
|
-
stream: bool
|
|
31
|
-
tools: Optional[
|
|
32
|
-
tool_choice: Optional[object]
|
|
33
|
-
parallel_tool_calls: Optional[bool]
|
|
28
|
+
model: 'str'
|
|
29
|
+
messages: 'typing.List[ChatMessage]'
|
|
30
|
+
stream: 'bool'
|
|
31
|
+
tools: 'Optional[typing.List[typing.Dict[str, object]]]'
|
|
32
|
+
tool_choice: 'Optional[object]'
|
|
33
|
+
parallel_tool_calls: 'Optional[bool]'
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
PayloadPostProcessor = Callable[[OutgoingRequest], OutgoingRequest]
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def _identity(outcomming_request: OutgoingRequest) -> OutgoingRequest:
|
|
39
|
+
def _identity(outcomming_request: 'OutgoingRequest') -> 'OutgoingRequest':
|
|
40
40
|
"""Keep the canonical request unchanged."""
|
|
41
41
|
|
|
42
42
|
return outcomming_request
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def _drop_developer_messages(outcomming_request: OutgoingRequest) -> OutgoingRequest:
|
|
45
|
+
def _drop_developer_messages(outcomming_request: 'OutgoingRequest') -> 'OutgoingRequest':
|
|
46
46
|
"""Remove all developer-role messages for providers that reject them."""
|
|
47
47
|
|
|
48
48
|
outcomming_request["messages"] = [
|
|
@@ -52,18 +52,27 @@ def _drop_developer_messages(outcomming_request: OutgoingRequest) -> OutgoingReq
|
|
|
52
52
|
]
|
|
53
53
|
return outcomming_request
|
|
54
54
|
|
|
55
|
+
def _replace_developer_messages(outcomming_request: 'OutgoingRequest') -> 'OutgoingRequest':
|
|
56
|
+
"""Replace all developer-role messages to system-role messages"""
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
for message in outcomming_request['messages']:
|
|
59
|
+
if message.get("role") == "developer":
|
|
60
|
+
message['role'] = "system"
|
|
61
|
+
|
|
62
|
+
return outcomming_request
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
PAYLOAD_POST_PROCESSORS: 'typing.Dict[str, PayloadPostProcessor]' = {
|
|
66
|
+
"stepfun": _replace_developer_messages,
|
|
58
67
|
"vllm": _identity,
|
|
59
68
|
}
|
|
60
69
|
"""Mapping from normalized `model_provider` name to payload rewrite hook."""
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
def post_process_outcomming_request(
|
|
64
|
-
outcomming_request: OutgoingRequest,
|
|
65
|
-
model_provider: str
|
|
66
|
-
) -> OutgoingRequest:
|
|
73
|
+
outcomming_request: 'OutgoingRequest',
|
|
74
|
+
model_provider: 'typing.Union[str, None]',
|
|
75
|
+
) -> 'OutgoingRequest':
|
|
67
76
|
"""Apply the provider-specific payload hook to one outgoing request.
|
|
68
77
|
|
|
69
78
|
This is the single wrapper around `PAYLOAD_POST_PROCESSORS`: it normalizes
|
responses_server/server.py
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
from .config import CompatServerConfig
|
|
4
3
|
from .payload_processors import post_process_outcomming_request
|
|
5
4
|
from .session_store import SessionStore
|
|
6
5
|
from .stream_router import StreamRouter
|
|
6
|
+
import typing
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class ResponseServer:
|
|
10
10
|
def __init__(
|
|
11
11
|
self,
|
|
12
|
-
config: CompatServerConfig,
|
|
13
|
-
session_store: SessionStore
|
|
14
|
-
stream_router: StreamRouter
|
|
15
|
-
) -> None:
|
|
12
|
+
config: 'CompatServerConfig',
|
|
13
|
+
session_store: 'typing.Union[SessionStore, None]' = None,
|
|
14
|
+
stream_router: 'typing.Union[StreamRouter, None]' = None,
|
|
15
|
+
) -> 'None':
|
|
16
16
|
self._config = config
|
|
17
17
|
self._session_store = session_store or SessionStore()
|
|
18
18
|
self._stream_router = stream_router or StreamRouter(config)
|
|
19
19
|
|
|
20
20
|
@property
|
|
21
|
-
def config(self) -> CompatServerConfig:
|
|
21
|
+
def config(self) -> 'CompatServerConfig':
|
|
22
22
|
return self._config
|
|
23
23
|
|
|
24
24
|
@property
|
|
25
|
-
def session_store(self) -> SessionStore:
|
|
25
|
+
def session_store(self) -> 'SessionStore':
|
|
26
26
|
return self._session_store
|
|
27
27
|
|
|
28
28
|
@property
|
|
29
|
-
def stream_router(self) -> StreamRouter:
|
|
29
|
+
def stream_router(self) -> 'StreamRouter':
|
|
30
30
|
return self._stream_router
|
|
31
31
|
|
|
32
|
-
def list_models(self) ->
|
|
32
|
+
def list_models(self) -> 'typing.Dict[str, object]':
|
|
33
33
|
return self._stream_router.list_models()
|
|
34
34
|
|
|
35
35
|
def start_response_stream(
|
|
36
36
|
self,
|
|
37
|
-
request_body:
|
|
38
|
-
request_headers:
|
|
37
|
+
request_body: 'typing.Dict[str, object]',
|
|
38
|
+
request_headers: 'typing.Dict[str, str]',
|
|
39
39
|
):
|
|
40
40
|
outcomming_request = self._stream_router.build_outcomming_request(request_body)
|
|
41
41
|
outcomming_request = post_process_outcomming_request(
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
from dataclasses import dataclass
|
|
4
3
|
import threading
|
|
5
4
|
import time
|
|
5
|
+
import typing
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
@dataclass(frozen=True,
|
|
8
|
+
@dataclass(frozen=True, )
|
|
9
9
|
class StoredResponse:
|
|
10
|
-
response_id: str
|
|
11
|
-
session_id: str
|
|
12
|
-
model: str
|
|
13
|
-
created_at: float
|
|
10
|
+
response_id: 'str'
|
|
11
|
+
session_id: 'typing.Union[str, None]'
|
|
12
|
+
model: 'str'
|
|
13
|
+
created_at: 'float'
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class SessionStore:
|
|
17
|
-
def __init__(self) -> None:
|
|
17
|
+
def __init__(self) -> 'None':
|
|
18
18
|
self._lock = threading.Lock()
|
|
19
19
|
self._next_response_number = 1
|
|
20
|
-
self._responses:
|
|
20
|
+
self._responses: 'typing.Dict[str, StoredResponse]' = {}
|
|
21
21
|
|
|
22
|
-
def create_response(self, session_id: str
|
|
22
|
+
def create_response(self, session_id: 'typing.Union[str, None]', model: 'str') -> 'StoredResponse':
|
|
23
23
|
with self._lock:
|
|
24
24
|
response_id = f"resp_{self._next_response_number:08d}"
|
|
25
25
|
self._next_response_number += 1
|
|
@@ -32,6 +32,6 @@ class SessionStore:
|
|
|
32
32
|
self._responses[response_id] = stored
|
|
33
33
|
return stored
|
|
34
34
|
|
|
35
|
-
def get_response(self, response_id: str) -> StoredResponse
|
|
35
|
+
def get_response(self, response_id: 'str') -> 'typing.Union[StoredResponse, None]':
|
|
36
36
|
with self._lock:
|
|
37
37
|
return self._responses.get(response_id)
|