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.
Files changed (60) hide show
  1. pycodex/__init__.py +5 -1
  2. pycodex/agent.py +89 -51
  3. pycodex/cli.py +152 -45
  4. pycodex/collaboration.py +6 -7
  5. pycodex/compat.py +99 -0
  6. pycodex/context.py +110 -87
  7. pycodex/doctor.py +40 -40
  8. pycodex/model.py +429 -90
  9. pycodex/portable.py +33 -33
  10. pycodex/portable_server.py +22 -21
  11. pycodex/prompts/models.json +30 -0
  12. pycodex/protocol.py +84 -86
  13. pycodex/runtime.py +36 -35
  14. pycodex/runtime_services.py +69 -69
  15. pycodex/tools/agent_tool_schemas.py +0 -2
  16. pycodex/tools/apply_patch_tool.py +45 -46
  17. pycodex/tools/base_tool.py +35 -36
  18. pycodex/tools/close_agent_tool.py +2 -4
  19. pycodex/tools/code_mode_manager.py +61 -61
  20. pycodex/tools/exec_command_tool.py +5 -6
  21. pycodex/tools/exec_runtime.js +3 -3
  22. pycodex/tools/exec_tool.py +2 -4
  23. pycodex/tools/grep_files_tool.py +10 -11
  24. pycodex/tools/list_dir_tool.py +8 -9
  25. pycodex/tools/read_file_tool.py +13 -14
  26. pycodex/tools/request_permissions_tool.py +2 -4
  27. pycodex/tools/request_user_input_tool.py +13 -14
  28. pycodex/tools/resume_agent_tool.py +2 -4
  29. pycodex/tools/send_input_tool.py +8 -9
  30. pycodex/tools/shell_command_tool.py +5 -6
  31. pycodex/tools/shell_tool.py +5 -6
  32. pycodex/tools/spawn_agent_tool.py +4 -5
  33. pycodex/tools/unified_exec_manager.py +62 -61
  34. pycodex/tools/update_plan_tool.py +4 -5
  35. pycodex/tools/view_image_tool.py +4 -5
  36. pycodex/tools/wait_agent_tool.py +2 -4
  37. pycodex/tools/wait_tool.py +4 -5
  38. pycodex/tools/web_search_tool.py +1 -3
  39. pycodex/tools/write_stdin_tool.py +4 -5
  40. pycodex/utils/__init__.py +4 -0
  41. pycodex/utils/compactor.py +189 -0
  42. pycodex/utils/dotenv.py +6 -6
  43. pycodex/utils/get_env.py +37 -33
  44. pycodex/utils/random_ids.py +1 -2
  45. pycodex/utils/session_persist.py +483 -0
  46. pycodex/utils/visualize.py +197 -83
  47. {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/METADATA +32 -11
  48. python_codex-0.1.4.dist-info/RECORD +76 -0
  49. {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/WHEEL +1 -1
  50. responses_server/app.py +32 -20
  51. responses_server/config.py +17 -17
  52. responses_server/payload_processors.py +26 -17
  53. responses_server/server.py +11 -11
  54. responses_server/session_store.py +10 -10
  55. responses_server/stream_router.py +83 -64
  56. responses_server/tools/custom_adapter.py +12 -12
  57. responses_server/tools/web_search.py +33 -33
  58. python_codex-0.1.2.dist-info/RECORD +0 -73
  59. {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/entry_points.txt +0 -0
  60. {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.4
1
+ Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.1.2
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.10
7
- Requires-Dist: cryptography>=3.4
8
- Requires-Dist: fastapi>=0.115
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.31
12
- Requires-Dist: tomli>=2.0; python_version < '3.11'
13
- Requires-Dist: uvicorn>=0.32
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 / hooks / review mode
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 `/model`
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.17.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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 _format_sse_event(event_name: str, payload: dict[str, object]) -> bytes:
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: dict[str, object], request_headers: dict[str, str]) -> Iterator[bytes]:
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": str(exc),
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 | None = None,
73
- model_provider: str | None = None,
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() -> dict[str, bool]:
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(target=self._server.run, daemon=True)
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))
@@ -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, slots=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 | None = None
14
- model_provider: str | None = None
15
- timeout_seconds: float = 120.0
16
-
17
- def outcomming_api_key(self) -> str | None:
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 | None = None,
46
- model_provider: str | None = None,
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 Optional, TypedDict
12
+ from typing import Callable, Optional
13
+ import typing
14
+ from typing_extensions import TypedDict
15
15
 
16
- ChatMessage = dict[str, object]
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: list[ChatMessage]
30
- stream: bool
31
- tools: Optional[list[dict[str, object]]]
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
- PAYLOAD_POST_PROCESSORS: dict[str, PayloadPostProcessor] = {
57
- "stepfun": _drop_developer_messages,
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 | None,
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
@@ -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 | None = None,
14
- stream_router: StreamRouter | None = None,
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) -> dict[str, object]:
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: dict[str, object],
38
- request_headers: dict[str, str],
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, slots=True)
8
+ @dataclass(frozen=True, )
9
9
  class StoredResponse:
10
- response_id: str
11
- session_id: str | None
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: dict[str, StoredResponse] = {}
20
+ self._responses: 'typing.Dict[str, StoredResponse]' = {}
21
21
 
22
- def create_response(self, session_id: str | None, model: str) -> StoredResponse:
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 | None:
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)