python-codex 0.2.7__py3-none-any.whl → 0.3.0__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 (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,90 @@
1
+ pycodex/__init__.py,sha256=4BBGUXImsNBdSQjp2KtshkjQK9BMz5gK93DzpWokkWY,3235
2
+ pycodex/agent.py,sha256=zCIG2DV0oQntLXHFj8FVRkgBp5TCC0YJ7sz0lni9J1s,21177
3
+ pycodex/bootstrap.py,sha256=DSVx8u94tHRYbUa_e9zdKPqozOv_CrfctbTKZ4JG064,13765
4
+ pycodex/cli.py,sha256=m5IJhnodNqsfMTIzdwbuaHBl2r-N8x1MxYSpe_hlHiM,12997
5
+ pycodex/compat.py,sha256=yoQQWMiAQ6ytZv3DAMIbp66ImA0kyY4G5eKHrtwmj6c,3490
6
+ pycodex/context.py,sha256=nkq2Y0coZNpWs5IJj7uVVT65fDqh58Z9c07y_Wy0f4Q,26604
7
+ pycodex/doctor.py,sha256=9p624b1zloXbhyMRi1-Of1YtnYeoF4zHxfGFhfujDS4,10620
8
+ pycodex/events.py,sha256=MZljtqra6-Hnt9Emjbn_wgp5cGaODx1w3CbmIKd5htI,29692
9
+ pycodex/feishu_card.py,sha256=Gclo4TI3KTfdT5NDLiv58CRg6hH3sUCRXuSrHgXgYjI,27767
10
+ pycodex/feishu_link.py,sha256=yUbEvO-8g-Bxo8_cF9s6vLZguz3NA6q3qt2A9Q09fsE,14523
11
+ pycodex/model.py,sha256=cKG2STZ_Kc3ThelY6KTTRkBTCrIwcmF74Rhfhj71N08,44175
12
+ pycodex/model_metadata.py,sha256=RMYhvgkm2v1nhCslSiDXPAwrRAx8FPz9QJqv8QTbWlc,1332
13
+ pycodex/portable.py,sha256=rfOpVnBZ01RS0Kyjo_w2FNKHY2t-lUcHVtWhPsbp1-k,16158
14
+ pycodex/portable_server.py,sha256=A4IY3xHtBQLRxdHQ4O5zdaVGjqENaLvypsSkLGZvCZ0,7498
15
+ pycodex/protocol.py,sha256=dRcTVKTaE7Eaq_jiSYdCzBMebIo3gnL7wB1x-NJqY5E,12027
16
+ pycodex/runtime.py,sha256=bRlqrmXqK6kPndAG96QoGALKicbWmfuXgo2jDvbQzdE,23806
17
+ pycodex/runtime_services.py,sha256=4jDCMlXHwgbJZPJkWzVEPCRHPxrD0mpCBbDInVToGG8,15033
18
+ pycodex/prompts/default_base_instructions.md,sha256=D65mcj6bo4CDvVom-D9cbJRJVNquo0NghKt164_fRsg,20923
19
+ pycodex/prompts/models.json,sha256=9UUBlKwpQSrt53WS-D--I3pQVBhRUe9XhDpEHUS6be8,330811
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=UYwmsX_Ckw_YaYQ6TYhUU6w5pxi2qzZoTrq6BAgNErM,1857
29
+ pycodex/tools/agent_tool_schemas.py,sha256=8cL9ml4So0H6IIeK0ZzjAo9bk3VeprDOSYPpIamOqEU,2048
30
+ pycodex/tools/apply_patch_tool.py,sha256=t8XFCGZ7q-s9C1Ty6qkReBRk-QD3ONvcKXLAz9djKRY,13956
31
+ pycodex/tools/base_tool.py,sha256=kNNzPWijtdl4C_GunyR5G1qvsr7KQtnFJ2RiRHHnAYo,6478
32
+ pycodex/tools/clock_tool.py,sha256=2_RYbBEw4V9_f1L-qZl3hhJ6XNPM_gBDmOlCh1cCAjo,6575
33
+ pycodex/tools/close_agent_tool.py,sha256=zQtvd5iGW6YCPFiec8zc2OdGtgNIxoA4p2ZTuCoJOaI,2031
34
+ pycodex/tools/code_mode_manager.py,sha256=VFVn7S_qUHCOHW952CMeGxTIE-7CPOx2uuXJZgBml8o,19160
35
+ pycodex/tools/exec_command_tool.py,sha256=iqRiKaaX-AjG6LjATAMOakHIP0gpCWCj2o8af_8eAiQ,4464
36
+ pycodex/tools/exec_runtime.js,sha256=DR1uocKailTqNWAcJNFJuQgFFMSUzTpT_uQsRaneg2s,3643
37
+ pycodex/tools/exec_tool.py,sha256=tuC21rlhP8CPSYml4XnSDSRV0UPGkQ6aHj0DLIoOOvM,2274
38
+ pycodex/tools/grep_files_tool.py,sha256=Ysw68VKI9OEvUH5haYx8FWuthkHykc1wskbuzjj8gTk,5293
39
+ pycodex/tools/ipython_tool.py,sha256=SKVYpJEwlYjOQZQWbpqvrA-3K9aJTlIG9KM50p5JYUM,4496
40
+ pycodex/tools/list_dir_tool.py,sha256=_U6ouRcVbGHT5mChVyUsiMgDGrFK7EFMFaspPLAHtH8,5337
41
+ pycodex/tools/read_file_tool.py,sha256=YK__uhIVldBjAQega5S_ZuZU5jgP_kLOwr-YhjAmRo4,9735
42
+ pycodex/tools/request_permissions_tool.py,sha256=n8eg29SgRbx1DUxxLbAztnc5i-0j2N2WnDupspjwWbE,3979
43
+ pycodex/tools/request_user_input_tool.py,sha256=_7siHF2p3KiNDFwsTqKKIUw2kPCXMzLaR12PkZ1wdIQ,4050
44
+ pycodex/tools/resume_agent_tool.py,sha256=D_OcWQbIbsWf-2NrRgDab4hB6BUbay0XidfPInhg88Y,1614
45
+ pycodex/tools/send_input_tool.py,sha256=J91LLz4RpvHmBWti0XZgI9xWipWwD8-1hQIdC8-Jd5E,3762
46
+ pycodex/tools/shell_command_tool.py,sha256=US63I5xiV5zfO1Oox2yHUg7F6XoKzmyxtohfaQIsmrw,4216
47
+ pycodex/tools/shell_tool.py,sha256=lqFH2XosfBg7yQKhpB7i4vLGz9sNQtEZihD0rjsmBCQ,4068
48
+ pycodex/tools/spawn_agent_tool.py,sha256=n_WZi1IX4Waj-GQ-hR1BVlk3zzw_ssvem7zcj3tYI38,6209
49
+ pycodex/tools/unified_exec_manager.py,sha256=sO1H3uoXyZ6DiyF98-eidQ77mS-P5gSWyPqM3L50yTQ,14538
50
+ pycodex/tools/update_plan_tool.py,sha256=8zyANrQgj8u2LBJ2yz_0Cg5jP3S6ibp6eASxUupX9C8,3199
51
+ pycodex/tools/view_image_tool.py,sha256=UZRkBEwuCkqxOw7XBwacdgPZAtbnD6uSHTdm-6amUo4,3902
52
+ pycodex/tools/wait_agent_tool.py,sha256=2OWfb9FCGu5SjbRp33dVEJWG3apMtQgczvQR1V_rm6I,3829
53
+ pycodex/tools/wait_tool.py,sha256=0_OO_DvMsBhG3oDKZePTOjZTBq1lmWntXAzqIqvebdA,3206
54
+ pycodex/tools/web_search_tool.py,sha256=3ao1IvJw7MwcP35UKLbqrM-nlm5Z-UUFVdEzkJYOV1o,988
55
+ pycodex/tools/write_stdin_tool.py,sha256=jvBDchLLmoDITd7g-ZVeEVXHRhWKkmnjM5laL-53FOM,3609
56
+ pycodex/utils/__init__.py,sha256=lbf-jbo6JS7t1wfFkQqLHlG4zZVqXYGkOIj2cvtrjx8,591
57
+ pycodex/utils/async_bridge.py,sha256=d21Pjim-nsQbSG5pJddd0WaQ03CzA3w3TINWDmmjWbg,1815
58
+ pycodex/utils/compactor.py,sha256=0tlszw1n54aMEGH6sL1U1dEgFSU5ioYL2NI5uSvHSiM,6119
59
+ pycodex/utils/debug.py,sha256=BdXmuAQVWVB1BONKRDlkMpgXpkt4M1jhvEAq5n2AumI,296
60
+ pycodex/utils/dotenv.py,sha256=h7ZgvqpEswetqjhtwfiaHyokAHwNKPakizTAHPHz5Ds,1865
61
+ pycodex/utils/event_helpers.py,sha256=pnxL0-nBC7cnOheHPCIfkHqJjMbkJg3C348_EzAVhFA,6342
62
+ pycodex/utils/get_env.py,sha256=L4S8h84tkgxZKvrxY7ezTgePbXepG-yIigUTKeY6uuE,6094
63
+ pycodex/utils/image_utils.py,sha256=1kXhtTqL4xtazpEABHhtvJBUJlceo8hSzg6ukmf_4FA,2407
64
+ pycodex/utils/random_ids.py,sha256=jLTxZZ0MBbRdQOFDFWGVRjWzIo6FKSQ5ADXbQSLPp1s,379
65
+ pycodex/utils/session_persist.py,sha256=v9KhmjUB2yCg55c85i8ZU1BWfc-WEMDPmpT0Rv4K8a8,22489
66
+ pycodex/utils/truncation.py,sha256=5D6vAorT8EgXgjd7ASwtraAyIZdlCR-w5jKZqJxDzho,5446
67
+ responses_server/__init__.py,sha256=sx9C88YWqmehASZy24FUMqT14A36doM_rj0MIZtQ9iQ,377
68
+ responses_server/__main__.py,sha256=876xZTqhPeDBD2GKKr8pKX33Af8PhFTosAI8bYQsFgQ,61
69
+ responses_server/app.py,sha256=DtA1Sck-6FDyCww3MLkLIMs-fDacY1d-lqZLWoUn-KM,7874
70
+ responses_server/config.py,sha256=sCJYGisPHW9Uup26-R9Zv0o9W_kRVt-ETc2MFCVjG90,2795
71
+ responses_server/messages_api.py,sha256=ialAYTMmN0y-uxQB3Y51ukq-kvkSVO84WVLXaqLsKw0,18775
72
+ responses_server/payload_processors.py,sha256=dqPwqHD-FhyiIwjzFEMf2wCtQDxkRyerZzbdqnH1aDE,3561
73
+ responses_server/server.py,sha256=k4DbsOYkaHHcy38uF9u_QXahJC1cplKsTu-MdC3DCIs,2286
74
+ responses_server/session_store.py,sha256=nwGxQL6nUVKnDVeDHz8IzqNQ7dWk8Ybf0URekOHdxTo,1133
75
+ responses_server/stream_router.py,sha256=xoHwegjziLOgrzvkdAtJbKGoSBn6sBF-unUSo6QxStc,41402
76
+ responses_server/trajectory_dump.py,sha256=5_Wy6tslszrqx3j1JeP_boyTbzIicKy8No84PzMnmHk,5111
77
+ responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
78
+ responses_server/tools/custom_adapter.py,sha256=SnIiNUdomff3APtTriURAC9YCeEdhCDjzGMEYusfUdk,8340
79
+ responses_server/tools/web_search.py,sha256=pnKH9JyTScRehWpZS3HpXRvjWsQvC0QZqxjEOCT4ndg,8936
80
+ workspace_server/__init__.py,sha256=PRM4ONODb6hxjBUHkBD4TprE_pkTCIlT9sfftK5ic6M,866
81
+ workspace_server/__main__.py,sha256=876xZTqhPeDBD2GKKr8pKX33Af8PhFTosAI8bYQsFgQ,61
82
+ workspace_server/app.py,sha256=iYUMziweCnsLX0EyT3jnswyVM4x2FhacrlinhBhymHo,59130
83
+ workspace_server/workspace.html,sha256=6UmGemYZfxFFX1qZNhGsVmD_BDWu6qhoPy3zyfd5_po,71483
84
+ workspace_server/workspaces.html,sha256=TpmfzpuCIXTVdFQzQrojxSCtBTHMSESn1zNGOmNz_W8,15787
85
+ workspace_server/workspaces.py,sha256=0oOk1xj7FxoJkLe2OyFDYVwWV4HHyNAwVaF-3Ln3NiY,22660
86
+ python_codex-0.3.0.dist-info/METADATA,sha256=Crsk3zCk6n-qeqnK7DoyH0GkSP4uUdf2f3TR5rTtUBs,35836
87
+ python_codex-0.3.0.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
88
+ python_codex-0.3.0.dist-info/entry_points.txt,sha256=vkV2UWCtEKvQNMJuPNjt8HyBKiwp83JyqBatrBNGDp8,80
89
+ python_codex-0.3.0.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
90
+ python_codex-0.3.0.dist-info/RECORD,,
@@ -1,8 +1,4 @@
1
- from .app import (
2
- ManagedResponseServer,
3
- launch_chat_completion_compat_server,
4
- run_server,
5
- )
1
+ from .app import ManagedResponseServer, launch_chat_completion_compat_server, run_server
6
2
  from .config import CompatServerConfig
7
3
  from .server import ResponseServer
8
4
  from .stream_router import StreamRouter
@@ -1,5 +1,4 @@
1
1
  from .app import main
2
2
 
3
-
4
3
  if __name__ == "__main__":
5
4
  main()
responses_server/app.py CHANGED
@@ -1,44 +1,52 @@
1
-
2
1
  import argparse
3
2
  import asyncio
4
- from dataclasses import replace
5
3
  import json
6
4
  import socket
7
5
  import threading
8
6
  import time
7
+ import typing
8
+ from dataclasses import replace
9
9
  from typing import Iterator
10
10
 
11
+ import uvicorn
11
12
  from fastapi import FastAPI, Request
12
13
  from fastapi.responses import JSONResponse, StreamingResponse
13
- import uvicorn
14
14
 
15
15
  from .config import CompatServerConfig
16
16
  from .server import ResponseServer
17
17
  from .stream_router import OutcommingChatError, UnsupportedIncommingFeature
18
- import typing
19
18
 
20
19
 
21
- def _run_uvicorn_server(server) -> 'None':
20
+ def _run_uvicorn_server(server) -> "None":
22
21
  asyncio.set_event_loop(asyncio.new_event_loop())
23
22
  server.run()
24
23
 
25
24
 
26
- def _format_sse_event(event_name: 'str', payload: 'typing.Dict[str, object]') -> 'bytes':
25
+ def _format_sse_event(
26
+ event_name: "str", payload: "typing.Dict[str, object]"
27
+ ) -> "bytes":
27
28
  data = json.dumps(payload, ensure_ascii=False)
28
29
  return f"event: {event_name}\ndata: {data}\n\n".encode("utf-8")
29
30
 
30
31
 
31
- def _stream_events(response_server: 'ResponseServer', request_body: 'typing.Dict[str, object]', request_headers: 'typing.Dict[str, str]') -> 'Iterator[bytes]':
32
+ def _stream_events(
33
+ response_server: "ResponseServer",
34
+ request_body: "typing.Dict[str, object]",
35
+ request_headers: "typing.Dict[str, str]",
36
+ ) -> "Iterator[bytes]":
32
37
  try:
33
- event_iter = response_server.start_response_stream(request_body, request_headers)
38
+ event_iter = response_server.start_response_stream(
39
+ request_body, request_headers
40
+ )
34
41
  for event_name, payload in event_iter:
35
42
  yield _format_sse_event(event_name, payload)
36
43
  except OutcommingChatError as exc:
37
-
44
+
38
45
  import traceback
46
+
39
47
  exc_info = traceback.format_exception(type(exc), exc, exc.__traceback__)
40
48
  error = {
41
- "message": '\n'.join(exc_info),
49
+ "message": "\n".join(exc_info),
42
50
  }
43
51
  error_type = getattr(exc, "error_type", None)
44
52
  if error_type:
@@ -54,7 +62,7 @@ def _stream_events(response_server: 'ResponseServer', request_body: 'typing.Dict
54
62
  )
55
63
 
56
64
 
57
- def build_parser() -> 'argparse.ArgumentParser':
65
+ def build_parser() -> "argparse.ArgumentParser":
58
66
  parser = argparse.ArgumentParser(
59
67
  prog="python -m responses_server",
60
68
  description=(
@@ -77,7 +85,7 @@ def build_parser() -> 'argparse.ArgumentParser':
77
85
  return parser
78
86
 
79
87
 
80
- def run_server(config: 'CompatServerConfig') -> 'None':
88
+ def run_server(config: "CompatServerConfig") -> "None":
81
89
  uvicorn.run(
82
90
  ManagedResponseServer.build_app(config),
83
91
  host=config.host,
@@ -87,10 +95,10 @@ def run_server(config: 'CompatServerConfig') -> 'None':
87
95
 
88
96
 
89
97
  def launch_chat_completion_compat_server(
90
- base_url: 'str',
91
- api_key_env: 'typing.Union[str, None]' = None,
92
- model_provider: 'typing.Union[str, None]' = None,
93
- outcomming_api: 'str' = "chat_completions",
98
+ base_url: "str",
99
+ api_key_env: "typing.Union[str, None]" = None,
100
+ model_provider: "typing.Union[str, None]" = None,
101
+ outcomming_api: "str" = "chat_completions",
94
102
  ):
95
103
  config = CompatServerConfig.from_base_url(
96
104
  base_url,
@@ -106,10 +114,10 @@ def launch_chat_completion_compat_server(
106
114
  class ManagedResponseServer:
107
115
  @staticmethod
108
116
  def build_app(
109
- config: 'CompatServerConfig',
117
+ config: "CompatServerConfig",
110
118
  session_store=None,
111
119
  stream_router=None,
112
- ) -> 'FastAPI':
120
+ ) -> "FastAPI":
113
121
  response_server = ResponseServer(
114
122
  config,
115
123
  session_store=session_store,
@@ -120,7 +128,7 @@ class ManagedResponseServer:
120
128
 
121
129
  @app.get("/health")
122
130
  @app.get("/healthz")
123
- async def health() -> 'typing.Dict[str, bool]':
131
+ async def health() -> "typing.Dict[str, bool]":
124
132
  return {"ok": True}
125
133
 
126
134
  @app.get("/models")
@@ -136,7 +144,7 @@ class ManagedResponseServer:
136
144
 
137
145
  @app.post("/responses")
138
146
  @app.post("/v1/responses")
139
- async def responses(request: 'Request'):
147
+ async def responses(request: "Request"):
140
148
  try:
141
149
  request_body = await request.json()
142
150
  except Exception as exc:
@@ -151,8 +159,7 @@ class ManagedResponseServer:
151
159
  )
152
160
 
153
161
  request_headers = {
154
- str(key).lower(): str(value)
155
- for key, value in request.headers.items()
162
+ str(key).lower(): str(value) for key, value in request.headers.items()
156
163
  }
157
164
  try:
158
165
  response_server.stream_router.validate_incomming_request(request_body)
@@ -173,7 +180,7 @@ class ManagedResponseServer:
173
180
 
174
181
  return app
175
182
 
176
- def __init__(self, config: 'CompatServerConfig') -> 'None':
183
+ def __init__(self, config: "CompatServerConfig") -> "None":
177
184
  port = config.port or _reserve_free_port()
178
185
  self._config = replace(config, port=port)
179
186
  self._app = self.build_app(self._config)
@@ -193,10 +200,10 @@ class ManagedResponseServer:
193
200
  )
194
201
 
195
202
  @property
196
- def base_url(self) -> 'str':
203
+ def base_url(self) -> "str":
197
204
  return f"http://{self._config.host}:{self._config.port}/v1"
198
205
 
199
- def start(self, timeout_seconds: 'float' = 10.0) -> 'None':
206
+ def start(self, timeout_seconds: "float" = 10.0) -> "None":
200
207
  self._thread.start()
201
208
  deadline = time.time() + timeout_seconds
202
209
  while not self._server.started:
@@ -206,16 +213,14 @@ class ManagedResponseServer:
206
213
  )
207
214
  time.sleep(0.01)
208
215
 
209
- def stop(self, timeout_seconds: 'float' = 5.0) -> 'None':
216
+ def stop(self, timeout_seconds: "float" = 5.0) -> "None":
210
217
  self._server.should_exit = True
211
218
  self._thread.join(timeout=timeout_seconds)
212
219
  if self._thread.is_alive():
213
- raise RuntimeError(
214
- "timed out waiting for managed responses server to stop"
215
- )
220
+ raise RuntimeError("timed out waiting for managed responses server to stop")
216
221
 
217
222
 
218
- def main() -> 'None':
223
+ def main() -> "None":
219
224
  args = build_parser().parse_args()
220
225
  run_server(
221
226
  CompatServerConfig(
@@ -234,7 +239,7 @@ if __name__ == "__main__":
234
239
  main()
235
240
 
236
241
 
237
- def _reserve_free_port() -> 'int':
242
+ def _reserve_free_port() -> "int":
238
243
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
239
244
  try:
240
245
  sock.bind(("127.0.0.1", 0))
@@ -1,46 +1,46 @@
1
-
2
1
  import os
3
- from dataclasses import dataclass
4
- import urllib.parse
5
2
  import typing
6
-
3
+ import urllib.parse
4
+ from dataclasses import dataclass
7
5
 
8
6
  DEFAULT_OUTCOMMING_TIMEOUT_SECONDS = 300.0
9
7
 
10
8
 
11
- @dataclass(frozen=True, )
9
+ @dataclass(
10
+ frozen=True,
11
+ )
12
12
  class CompatServerConfig:
13
- host: 'str' = "127.0.0.1"
14
- port: 'int' = 0
15
- outcomming_base_url: 'str' = "http://127.0.0.1:8000/v1"
16
- outcomming_api: 'str' = "chat_completions"
17
- outcomming_api_key_env: 'typing.Union[str, None]' = None
18
- model_provider: 'typing.Union[str, None]' = None
19
- timeout_seconds: 'float' = DEFAULT_OUTCOMMING_TIMEOUT_SECONDS
13
+ host: "str" = "127.0.0.1"
14
+ port: "int" = 0
15
+ outcomming_base_url: "str" = "http://127.0.0.1:8000/v1"
16
+ outcomming_api: "str" = "chat_completions"
17
+ outcomming_api_key_env: "typing.Union[str, None]" = None
18
+ model_provider: "typing.Union[str, None]" = None
19
+ timeout_seconds: "float" = DEFAULT_OUTCOMMING_TIMEOUT_SECONDS
20
20
 
21
- def outcomming_api_key(self) -> 'typing.Union[str, None]':
21
+ def outcomming_api_key(self) -> "typing.Union[str, None]":
22
22
  if self.outcomming_api_key_env is None:
23
23
  return None
24
24
  value = os.environ.get(self.outcomming_api_key_env, "").strip()
25
25
  return value or None
26
26
 
27
- def outcomming_chat_completions_url(self) -> 'str':
27
+ def outcomming_chat_completions_url(self) -> "str":
28
28
  base = self.outcomming_base_url.rstrip("/")
29
29
  return f"{base}/chat/completions"
30
30
 
31
- def outcomming_messages_url(self) -> 'str':
31
+ def outcomming_messages_url(self) -> "str":
32
32
  base = self.outcomming_base_url.rstrip("/")
33
33
  return f"{base}/messages"
34
34
 
35
- def outcomming_models_url(self) -> 'str':
35
+ def outcomming_models_url(self) -> "str":
36
36
  base = self.outcomming_base_url.rstrip("/")
37
37
  return f"{base}/models"
38
38
 
39
- def normalized_outcomming_api(self) -> 'str':
39
+ def normalized_outcomming_api(self) -> "str":
40
40
  value = str(self.outcomming_api or "").strip().lower()
41
41
  return value or "chat_completions"
42
42
 
43
- def with_ephemeral_port(self) -> 'CompatServerConfig':
43
+ def with_ephemeral_port(self) -> "CompatServerConfig":
44
44
  return CompatServerConfig(
45
45
  host=self.host,
46
46
  port=0,
@@ -54,11 +54,11 @@ class CompatServerConfig:
54
54
  @classmethod
55
55
  def from_base_url(
56
56
  cls,
57
- outcomming_base_url: 'str',
58
- api_key_env: 'typing.Union[str, None]' = None,
59
- model_provider: 'typing.Union[str, None]' = None,
60
- outcomming_api: 'str' = "chat_completions",
61
- ) -> 'CompatServerConfig':
57
+ outcomming_base_url: "str",
58
+ api_key_env: "typing.Union[str, None]" = None,
59
+ model_provider: "typing.Union[str, None]" = None,
60
+ outcomming_api: "str" = "chat_completions",
61
+ ) -> "CompatServerConfig":
62
62
  parsed = urllib.parse.urlparse(outcomming_base_url)
63
63
  if not parsed.scheme or not parsed.netloc:
64
64
  raise ValueError(f"invalid outcomming base url: {outcomming_base_url}")
@@ -9,8 +9,8 @@ class MessagesAPIAdapterError(ValueError):
9
9
 
10
10
 
11
11
  def build_messages_request(
12
- outcomming_request: 'typing.Dict[str, object]',
13
- ) -> 'typing.Dict[str, object]':
12
+ outcomming_request: "typing.Dict[str, object]",
13
+ ) -> "typing.Dict[str, object]":
14
14
  model = str(outcomming_request.get("model", "")).strip()
15
15
  if not model:
16
16
  raise MessagesAPIAdapterError("outcomming request is missing `model`")
@@ -19,13 +19,11 @@ def build_messages_request(
19
19
  if not isinstance(raw_messages, list):
20
20
  raise MessagesAPIAdapterError("outcomming request `messages` must be a list")
21
21
 
22
- system_blocks: 'typing.List[typing.Dict[str, object]]' = []
23
- messages: 'typing.List[typing.Dict[str, object]]' = []
22
+ system_blocks: "typing.List[typing.Dict[str, object]]" = []
23
+ messages: "typing.List[typing.Dict[str, object]]" = []
24
24
  for raw_message in raw_messages:
25
25
  if not isinstance(raw_message, dict):
26
- raise MessagesAPIAdapterError(
27
- "outcomming request messages must be objects"
28
- )
26
+ raise MessagesAPIAdapterError("outcomming request messages must be objects")
29
27
  role = str(raw_message.get("role", "")).strip()
30
28
  if role in {"developer", "system"}:
31
29
  text = str(raw_message.get("content", "") or "")
@@ -60,7 +58,7 @@ def build_messages_request(
60
58
  f"unsupported outcomming message role for messages API: {role!r}"
61
59
  )
62
60
 
63
- payload: 'typing.Dict[str, object]' = {
61
+ payload: "typing.Dict[str, object]" = {
64
62
  "model": model,
65
63
  "messages": messages,
66
64
  "max_tokens": _resolve_max_tokens(outcomming_request),
@@ -84,12 +82,12 @@ def build_messages_request(
84
82
 
85
83
 
86
84
  def iter_chat_chunks(
87
- event_name: 'typing.Union[str, None]',
88
- payload: 'typing.Dict[str, object]',
89
- state: 'typing.Dict[str, object]',
90
- ) -> 'typing.List[typing.Dict[str, object]]':
85
+ event_name: "typing.Union[str, None]",
86
+ payload: "typing.Dict[str, object]",
87
+ state: "typing.Dict[str, object]",
88
+ ) -> "typing.List[typing.Dict[str, object]]":
91
89
  event_type = str(payload.get("type") or event_name or "").strip()
92
- chunks: 'typing.List[typing.Dict[str, object]]' = []
90
+ chunks: "typing.List[typing.Dict[str, object]]" = []
93
91
 
94
92
  if event_type == "message_start":
95
93
  message = payload.get("message") or {}
@@ -188,13 +186,15 @@ def iter_chat_chunks(
188
186
  return chunks
189
187
 
190
188
 
191
- def saw_message_stop(state: 'typing.Dict[str, object]') -> 'bool':
189
+ def saw_message_stop(state: "typing.Dict[str, object]") -> "bool":
192
190
  return bool(state.get("saw_message_stop"))
193
191
 
194
192
 
195
- def _build_text_blocks(raw_content: 'object') -> 'typing.List[typing.Dict[str, object]]':
193
+ def _build_text_blocks(
194
+ raw_content: "object",
195
+ ) -> "typing.List[typing.Dict[str, object]]":
196
196
  if isinstance(raw_content, list):
197
- blocks: 'typing.List[typing.Dict[str, object]]' = []
197
+ blocks: "typing.List[typing.Dict[str, object]]" = []
198
198
  for raw_part in raw_content:
199
199
  if not isinstance(raw_part, dict):
200
200
  raise MessagesAPIAdapterError("message content parts must be objects")
@@ -219,8 +219,8 @@ def _build_text_blocks(raw_content: 'object') -> 'typing.List[typing.Dict[str, o
219
219
 
220
220
 
221
221
  def _build_image_block(
222
- raw_part: 'typing.Dict[str, object]',
223
- ) -> 'typing.Dict[str, object]':
222
+ raw_part: "typing.Dict[str, object]",
223
+ ) -> "typing.Dict[str, object]":
224
224
  image_url = raw_part.get("image_url") or {}
225
225
  if not isinstance(image_url, dict):
226
226
  raise MessagesAPIAdapterError("`image_url` content parts must be objects")
@@ -233,7 +233,7 @@ def _build_image_block(
233
233
  return {"type": "image", "source": {"type": "url", "url": url}}
234
234
 
235
235
  header, _, data = url.partition(",")
236
- media_type = header[len("data:"):].split(";")[0].strip()
236
+ media_type = header[len("data:") :].split(";")[0].strip()
237
237
  if not media_type or not header.endswith(";base64"):
238
238
  raise MessagesAPIAdapterError(
239
239
  "`image_url` data URLs must be base64 encoded with a media type"
@@ -249,9 +249,9 @@ def _build_image_block(
249
249
 
250
250
 
251
251
  def _build_assistant_blocks(
252
- raw_message: 'typing.Dict[str, object]',
253
- ) -> 'typing.List[typing.Dict[str, object]]':
254
- blocks: 'typing.List[typing.Dict[str, object]]' = []
252
+ raw_message: "typing.Dict[str, object]",
253
+ ) -> "typing.List[typing.Dict[str, object]]":
254
+ blocks: "typing.List[typing.Dict[str, object]]" = []
255
255
  reasoning = str(raw_message.get("reasoning", "") or "")
256
256
  if reasoning:
257
257
  blocks.append({"type": "thinking", "thinking": reasoning})
@@ -284,8 +284,8 @@ def _build_assistant_blocks(
284
284
 
285
285
 
286
286
  def _build_tool_result_block(
287
- raw_message: 'typing.Dict[str, object]',
288
- ) -> 'typing.Dict[str, object]':
287
+ raw_message: "typing.Dict[str, object]",
288
+ ) -> "typing.Dict[str, object]":
289
289
  return {
290
290
  "type": "tool_result",
291
291
  "tool_use_id": str(raw_message.get("tool_call_id", "")).strip(),
@@ -294,9 +294,9 @@ def _build_tool_result_block(
294
294
 
295
295
 
296
296
  def _translate_tools(
297
- raw_tools: 'object',
298
- ) -> 'typing.List[typing.Dict[str, object]]':
299
- translated: 'typing.List[typing.Dict[str, object]]' = []
297
+ raw_tools: "object",
298
+ ) -> "typing.List[typing.Dict[str, object]]":
299
+ translated: "typing.List[typing.Dict[str, object]]" = []
300
300
  if not isinstance(raw_tools, list):
301
301
  return translated
302
302
  for raw_tool in raw_tools:
@@ -321,9 +321,9 @@ def _translate_tools(
321
321
 
322
322
 
323
323
  def _translate_tool_choice(
324
- raw_tool_choice: 'object',
325
- parallel_tool_calls: 'object',
326
- ) -> 'typing.Union[typing.Dict[str, object], None]':
324
+ raw_tool_choice: "object",
325
+ parallel_tool_calls: "object",
326
+ ) -> "typing.Union[typing.Dict[str, object], None]":
327
327
  if raw_tool_choice is None:
328
328
  if parallel_tool_calls is False:
329
329
  return {
@@ -332,7 +332,7 @@ def _translate_tool_choice(
332
332
  }
333
333
  return None
334
334
 
335
- translated: 'typing.Dict[str, object]'
335
+ translated: "typing.Dict[str, object]"
336
336
  if isinstance(raw_tool_choice, str):
337
337
  choice = raw_tool_choice.strip()
338
338
  if choice == "auto":
@@ -355,9 +355,7 @@ def _translate_tool_choice(
355
355
  if not name:
356
356
  name = str(raw_tool_choice.get("name", "")).strip()
357
357
  if not name:
358
- raise MessagesAPIAdapterError(
359
- "function tool_choice is missing `name`"
360
- )
358
+ raise MessagesAPIAdapterError("function tool_choice is missing `name`")
361
359
  translated = {
362
360
  "type": "tool",
363
361
  "name": name,
@@ -376,7 +374,7 @@ def _translate_tool_choice(
376
374
  return translated
377
375
 
378
376
 
379
- def _parse_json_object(raw_value: 'object') -> 'typing.Dict[str, object]':
377
+ def _parse_json_object(raw_value: "object") -> "typing.Dict[str, object]":
380
378
  if isinstance(raw_value, dict):
381
379
  return dict(raw_value)
382
380
  if isinstance(raw_value, str):
@@ -399,7 +397,7 @@ def _parse_json_object(raw_value: 'object') -> 'typing.Dict[str, object]':
399
397
  )
400
398
 
401
399
 
402
- def _resolve_max_tokens(outcomming_request: 'typing.Dict[str, object]') -> 'int':
400
+ def _resolve_max_tokens(outcomming_request: "typing.Dict[str, object]") -> "int":
403
401
  raw_value = outcomming_request.get("max_tokens")
404
402
  if isinstance(raw_value, bool):
405
403
  return DEFAULT_MESSAGES_MAX_TOKENS
@@ -408,7 +406,7 @@ def _resolve_max_tokens(outcomming_request: 'typing.Dict[str, object]') -> 'int'
408
406
  return DEFAULT_MESSAGES_MAX_TOKENS
409
407
 
410
408
 
411
- def _usage_chunk(raw_usage: 'object') -> 'typing.Union[typing.Dict[str, object], None]':
409
+ def _usage_chunk(raw_usage: "object") -> "typing.Union[typing.Dict[str, object], None]":
412
410
  usage = _translate_usage(raw_usage)
413
411
  if not usage:
414
412
  return None
@@ -418,10 +416,10 @@ def _usage_chunk(raw_usage: 'object') -> 'typing.Union[typing.Dict[str, object],
418
416
  }
419
417
 
420
418
 
421
- def _translate_usage(raw_usage: 'object') -> 'typing.Dict[str, object]':
419
+ def _translate_usage(raw_usage: "object") -> "typing.Dict[str, object]":
422
420
  if not isinstance(raw_usage, dict):
423
421
  return {}
424
- usage: 'typing.Dict[str, object]' = {}
422
+ usage: "typing.Dict[str, object]" = {}
425
423
  input_tokens = raw_usage.get("input_tokens")
426
424
  output_tokens = raw_usage.get("output_tokens")
427
425
  if isinstance(input_tokens, int):
@@ -434,7 +432,7 @@ def _translate_usage(raw_usage: 'object') -> 'typing.Dict[str, object]':
434
432
  elif isinstance(input_tokens, int) and isinstance(output_tokens, int):
435
433
  usage["total_tokens"] = input_tokens + output_tokens
436
434
 
437
- input_details: 'typing.Dict[str, int]' = {}
435
+ input_details: "typing.Dict[str, int]" = {}
438
436
  cache_creation = raw_usage.get("cache_creation_input_tokens")
439
437
  if isinstance(cache_creation, int):
440
438
  input_details["cache_creation_input_tokens"] = cache_creation
@@ -446,7 +444,7 @@ def _translate_usage(raw_usage: 'object') -> 'typing.Dict[str, object]':
446
444
  return usage
447
445
 
448
446
 
449
- def _normalize_index(raw_index: 'object') -> 'int':
447
+ def _normalize_index(raw_index: "object") -> "int":
450
448
  if isinstance(raw_index, int):
451
449
  return raw_index
452
450
  try:
@@ -455,7 +453,7 @@ def _normalize_index(raw_index: 'object') -> 'int':
455
453
  return 0
456
454
 
457
455
 
458
- def _translate_stop_reason(raw_stop_reason: 'object') -> 'typing.Union[str, None]':
456
+ def _translate_stop_reason(raw_stop_reason: "object") -> "typing.Union[str, None]":
459
457
  if not isinstance(raw_stop_reason, str):
460
458
  return None
461
459
  stop_reason = raw_stop_reason.strip()
@@ -470,21 +468,21 @@ def _translate_stop_reason(raw_stop_reason: 'object') -> 'typing.Union[str, None
470
468
  return stop_reason
471
469
 
472
470
 
473
- def _chat_text_chunk(text: 'str') -> 'typing.Dict[str, object]':
471
+ def _chat_text_chunk(text: "str") -> "typing.Dict[str, object]":
474
472
  return _chat_delta_chunk({"content": text})
475
473
 
476
474
 
477
- def _chat_reasoning_chunk(reasoning: 'str') -> 'typing.Dict[str, object]':
475
+ def _chat_reasoning_chunk(reasoning: "str") -> "typing.Dict[str, object]":
478
476
  return _chat_delta_chunk({"reasoning_content": reasoning})
479
477
 
480
478
 
481
479
  def _chat_tool_chunk(
482
- index: 'int',
483
- call_id: 'str' = "",
484
- name: 'str' = "",
485
- arguments: 'str' = "",
486
- ) -> 'typing.Dict[str, object]':
487
- tool_call: 'typing.Dict[str, object]' = {
480
+ index: "int",
481
+ call_id: "str" = "",
482
+ name: "str" = "",
483
+ arguments: "str" = "",
484
+ ) -> "typing.Dict[str, object]":
485
+ tool_call: "typing.Dict[str, object]" = {
488
486
  "index": index,
489
487
  "function": {},
490
488
  }
@@ -502,7 +500,7 @@ def _chat_tool_chunk(
502
500
  return _chat_delta_chunk({"tool_calls": [tool_call]})
503
501
 
504
502
 
505
- def _chat_delta_chunk(delta: 'typing.Dict[str, object]') -> 'typing.Dict[str, object]':
503
+ def _chat_delta_chunk(delta: "typing.Dict[str, object]") -> "typing.Dict[str, object]":
506
504
  return {
507
505
  "choices": [
508
506
  {
@@ -514,7 +512,7 @@ def _chat_delta_chunk(delta: 'typing.Dict[str, object]') -> 'typing.Dict[str, ob
514
512
  }
515
513
 
516
514
 
517
- def _chat_finish_chunk(finish_reason: 'str') -> 'typing.Dict[str, object]':
515
+ def _chat_finish_chunk(finish_reason: "str") -> "typing.Dict[str, object]":
518
516
  return {
519
517
  "choices": [
520
518
  {
@@ -526,5 +524,5 @@ def _chat_finish_chunk(finish_reason: 'str') -> 'typing.Dict[str, object]':
526
524
  }
527
525
 
528
526
 
529
- def _dump_json(raw_value: 'object') -> 'str':
527
+ def _dump_json(raw_value: "object") -> "str":
530
528
  return json.dumps(raw_value, ensure_ascii=False, separators=(",", ":"))