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
@@ -1,4 +1,3 @@
1
-
2
1
  """Provider-specific post-process hooks for canonical outgoing chat requests.
3
2
 
4
3
  Each downstream chat-completions provider may have its own payload quirks:
@@ -8,9 +7,10 @@ building one canonical `outcomming_request`, while `server.py` selects the
8
7
  appropriate hook from `CompatServerConfig.model_provider`.
9
8
  """
10
9
 
10
+ import typing
11
11
  from copy import deepcopy
12
12
  from typing import Callable, Optional
13
- import typing
13
+
14
14
  from typing_extensions import TypedDict
15
15
 
16
16
  ChatMessage = typing.Dict[str, object]
@@ -25,27 +25,29 @@ class OutgoingRequest(TypedDict):
25
25
  not rely on TypedDict inheritance.
26
26
  """
27
27
 
28
- model: 'str'
29
- messages: 'typing.List[ChatMessage]'
30
- stream: 'bool'
31
- chat_template_kwargs: 'Optional[typing.Dict[str, object]]'
32
- max_tokens: 'Optional[int]'
33
- tools: 'Optional[typing.List[typing.Dict[str, object]]]'
34
- tool_choice: 'Optional[object]'
35
- parallel_tool_calls: 'Optional[bool]'
36
- return_token_ids: 'Optional[bool]'
28
+ model: "str"
29
+ messages: "typing.List[ChatMessage]"
30
+ stream: "bool"
31
+ chat_template_kwargs: "Optional[typing.Dict[str, object]]"
32
+ max_tokens: "Optional[int]"
33
+ tools: "Optional[typing.List[typing.Dict[str, object]]]"
34
+ tool_choice: "Optional[object]"
35
+ parallel_tool_calls: "Optional[bool]"
36
+ return_token_ids: "Optional[bool]"
37
37
 
38
38
 
39
39
  PayloadPostProcessor = Callable[[OutgoingRequest], OutgoingRequest]
40
40
 
41
41
 
42
- def _identity(outcomming_request: 'OutgoingRequest') -> 'OutgoingRequest':
42
+ def _identity(outcomming_request: "OutgoingRequest") -> "OutgoingRequest":
43
43
  """Keep the canonical request unchanged."""
44
44
 
45
45
  return outcomming_request
46
46
 
47
47
 
48
- def _drop_developer_messages(outcomming_request: 'OutgoingRequest') -> 'OutgoingRequest':
48
+ def _drop_developer_messages(
49
+ outcomming_request: "OutgoingRequest",
50
+ ) -> "OutgoingRequest":
49
51
  """Remove all developer-role messages for providers that reject them."""
50
52
 
51
53
  outcomming_request["messages"] = [
@@ -55,17 +57,20 @@ def _drop_developer_messages(outcomming_request: 'OutgoingRequest') -> 'Outgoing
55
57
  ]
56
58
  return outcomming_request
57
59
 
58
- def _replace_developer_messages(outcomming_request: 'OutgoingRequest') -> 'OutgoingRequest':
60
+
61
+ def _replace_developer_messages(
62
+ outcomming_request: "OutgoingRequest",
63
+ ) -> "OutgoingRequest":
59
64
  """Replace all developer-role messages to system-role messages"""
60
65
 
61
- for message in outcomming_request['messages']:
66
+ for message in outcomming_request["messages"]:
62
67
  if message.get("role") == "developer":
63
- message['role'] = "system"
68
+ message["role"] = "system"
64
69
 
65
70
  return outcomming_request
66
71
 
67
72
 
68
- PAYLOAD_POST_PROCESSORS: 'typing.Dict[str, PayloadPostProcessor]' = {
73
+ PAYLOAD_POST_PROCESSORS: "typing.Dict[str, PayloadPostProcessor]" = {
69
74
  "stepfun": _replace_developer_messages,
70
75
  "vllm": _identity,
71
76
  }
@@ -73,9 +78,9 @@ PAYLOAD_POST_PROCESSORS: 'typing.Dict[str, PayloadPostProcessor]' = {
73
78
 
74
79
 
75
80
  def post_process_outcomming_request(
76
- outcomming_request: 'OutgoingRequest',
77
- model_provider: 'typing.Union[str, None]',
78
- ) -> 'OutgoingRequest':
81
+ outcomming_request: "OutgoingRequest",
82
+ model_provider: "typing.Union[str, None]",
83
+ ) -> "OutgoingRequest":
79
84
  """Apply the provider-specific payload hook to one outgoing request.
80
85
 
81
86
  This is the single wrapper around `PAYLOAD_POST_PROCESSORS`: it normalizes
@@ -1,43 +1,43 @@
1
+ import typing
1
2
 
2
3
  from .config import CompatServerConfig
3
4
  from .payload_processors import post_process_outcomming_request
4
5
  from .session_store import SessionStore
5
6
  from .stream_router import StreamRouter
6
7
  from .trajectory_dump import TrajectoryDumpWriter
7
- import typing
8
8
 
9
9
 
10
10
  class ResponseServer:
11
11
  def __init__(
12
12
  self,
13
- config: 'CompatServerConfig',
14
- session_store: 'typing.Union[SessionStore, None]' = None,
15
- stream_router: 'typing.Union[StreamRouter, None]' = None,
16
- ) -> 'None':
13
+ config: "CompatServerConfig",
14
+ session_store: "typing.Union[SessionStore, None]" = None,
15
+ stream_router: "typing.Union[StreamRouter, None]" = None,
16
+ ) -> "None":
17
17
  self._config = config
18
18
  self._session_store = session_store or SessionStore()
19
19
  self._stream_router = stream_router or StreamRouter(config)
20
20
  self._trajectory_dump = TrajectoryDumpWriter.from_env()
21
21
 
22
22
  @property
23
- def config(self) -> 'CompatServerConfig':
23
+ def config(self) -> "CompatServerConfig":
24
24
  return self._config
25
25
 
26
26
  @property
27
- def session_store(self) -> 'SessionStore':
27
+ def session_store(self) -> "SessionStore":
28
28
  return self._session_store
29
29
 
30
30
  @property
31
- def stream_router(self) -> 'StreamRouter':
31
+ def stream_router(self) -> "StreamRouter":
32
32
  return self._stream_router
33
33
 
34
- def list_models(self) -> 'typing.Dict[str, object]':
34
+ def list_models(self) -> "typing.Dict[str, object]":
35
35
  return self._stream_router.list_models()
36
36
 
37
37
  def start_response_stream(
38
38
  self,
39
- request_body: 'typing.Dict[str, object]',
40
- request_headers: 'typing.Dict[str, str]',
39
+ request_body: "typing.Dict[str, object]",
40
+ request_headers: "typing.Dict[str, str]",
41
41
  ):
42
42
  outcomming_request = self._stream_router.build_outcomming_request(request_body)
43
43
  if self._trajectory_dump is not None:
@@ -1,25 +1,28 @@
1
-
2
- from dataclasses import dataclass
3
1
  import threading
4
2
  import time
5
3
  import typing
4
+ from dataclasses import dataclass
6
5
 
7
6
 
8
- @dataclass(frozen=True, )
7
+ @dataclass(
8
+ frozen=True,
9
+ )
9
10
  class StoredResponse:
10
- response_id: 'str'
11
- session_id: 'typing.Union[str, None]'
12
- model: 'str'
13
- created_at: 'float'
11
+ response_id: "str"
12
+ session_id: "typing.Union[str, None]"
13
+ model: "str"
14
+ created_at: "float"
14
15
 
15
16
 
16
17
  class SessionStore:
17
- def __init__(self) -> 'None':
18
+ def __init__(self) -> "None":
18
19
  self._lock = threading.Lock()
19
20
  self._next_response_number = 1
20
- self._responses: 'typing.Dict[str, StoredResponse]' = {}
21
+ self._responses: "typing.Dict[str, StoredResponse]" = {}
21
22
 
22
- def create_response(self, session_id: 'typing.Union[str, None]', model: 'str') -> 'StoredResponse':
23
+ def create_response(
24
+ self, session_id: "typing.Union[str, None]", model: "str"
25
+ ) -> "StoredResponse":
23
26
  with self._lock:
24
27
  response_id = f"resp_{self._next_response_number:08d}"
25
28
  self._next_response_number += 1
@@ -32,6 +35,6 @@ class SessionStore:
32
35
  self._responses[response_id] = stored
33
36
  return stored
34
37
 
35
- def get_response(self, response_id: 'str') -> 'typing.Union[StoredResponse, None]':
38
+ def get_response(self, response_id: "str") -> "typing.Union[StoredResponse, None]":
36
39
  with self._lock:
37
40
  return self._responses.get(response_id)
@@ -1,25 +1,20 @@
1
-
2
- import json
3
1
  import http.client
2
+ import json
4
3
  import ssl
4
+ import typing
5
5
  import urllib.error
6
6
  import urllib.request
7
7
 
8
8
  from .config import CompatServerConfig
9
- from .messages_api import (
10
- MessagesAPIAdapterError,
11
- build_messages_request,
12
- iter_chat_chunks as iter_chat_chunks_from_messages,
13
- saw_message_stop as messages_saw_message_stop,
14
- )
9
+ from .messages_api import MessagesAPIAdapterError, build_messages_request
10
+ from .messages_api import iter_chat_chunks as iter_chat_chunks_from_messages
11
+ from .messages_api import saw_message_stop as messages_saw_message_stop
15
12
  from .session_store import StoredResponse
16
13
  from .tools import WebSearchTool, collect_custom_tool_names
17
- from .tools.custom_adapter import (
18
- CustomToolAdapterError,
19
- build_output_item as build_custom_output_item,
20
- build_tool_call as build_custom_tool_call,
21
- build_tool_definition as build_custom_tool_definition,
22
- )
14
+ from .tools.custom_adapter import CustomToolAdapterError
15
+ from .tools.custom_adapter import build_output_item as build_custom_output_item
16
+ from .tools.custom_adapter import build_tool_call as build_custom_tool_call
17
+ from .tools.custom_adapter import build_tool_definition as build_custom_tool_definition
23
18
  from .tools.web_search import (
24
19
  build_followup_request,
25
20
  build_output_items,
@@ -28,7 +23,6 @@ from .tools.web_search import (
28
23
  partition_tool_calls,
29
24
  )
30
25
  from .trajectory_dump import TrajectoryDumpWriter
31
- import typing
32
26
 
33
27
 
34
28
  class UnsupportedIncommingFeature(ValueError):
@@ -38,23 +32,23 @@ class UnsupportedIncommingFeature(ValueError):
38
32
  class OutcommingChatError(RuntimeError):
39
33
  def __init__(
40
34
  self,
41
- message: 'str',
42
- error_type: 'typing.Union[str, None]' = None,
43
- ) -> 'None':
35
+ message: "str",
36
+ error_type: "typing.Union[str, None]" = None,
37
+ ) -> "None":
44
38
  super().__init__(message)
45
39
  self.error_type = error_type
46
40
 
47
41
 
48
42
  class StreamRouter:
49
- def __init__(self, config: 'CompatServerConfig') -> 'None':
43
+ def __init__(self, config: "CompatServerConfig") -> "None":
50
44
  self._config = config
51
45
  self._mock_web_search = WebSearchTool()
52
46
 
53
47
  def _provider_capability(
54
48
  self,
55
- explicit_support: 'typing.Dict[str, bool]',
56
- default: 'typing.Union[bool, None]' = None,
57
- ) -> 'bool':
49
+ explicit_support: "typing.Dict[str, bool]",
50
+ default: "typing.Union[bool, None]" = None,
51
+ ) -> "bool":
58
52
  provider_name = str(self._config.model_provider or "").strip().lower()
59
53
  if provider_name in explicit_support:
60
54
  return explicit_support[provider_name]
@@ -64,7 +58,7 @@ class StreamRouter:
64
58
  return default
65
59
  raise KeyError("provider capability map is missing `vllm` fallback")
66
60
 
67
- def _supports_chat_reasoning(self) -> 'bool':
61
+ def _supports_chat_reasoning(self) -> "bool":
68
62
  # Unknown providers inherit the vLLM compatibility behavior unless a
69
63
  # provider is explicitly declared otherwise.
70
64
  return self._provider_capability(
@@ -74,7 +68,7 @@ class StreamRouter:
74
68
  }
75
69
  )
76
70
 
77
- def _supports_stream_usage(self) -> 'bool':
71
+ def _supports_stream_usage(self) -> "bool":
78
72
  return self._provider_capability(
79
73
  {
80
74
  "vllm": True,
@@ -84,8 +78,8 @@ class StreamRouter:
84
78
 
85
79
  def validate_incomming_request(
86
80
  self,
87
- incomming_request: 'typing.Dict[str, object]',
88
- ) -> 'None':
81
+ incomming_request: "typing.Dict[str, object]",
82
+ ) -> "None":
89
83
  model = str(incomming_request.get("model", "")).strip()
90
84
  if not model:
91
85
  raise UnsupportedIncommingFeature("incomming request is missing `model`")
@@ -104,11 +98,11 @@ class StreamRouter:
104
98
 
105
99
  def collect_custom_tool_names(
106
100
  self,
107
- incomming_request: 'typing.Dict[str, object]',
108
- ) -> 'typing.Set[str]':
101
+ incomming_request: "typing.Dict[str, object]",
102
+ ) -> "typing.Set[str]":
109
103
  return collect_custom_tool_names(incomming_request.get("tools") or [])
110
104
 
111
- def list_models(self) -> 'typing.Dict[str, object]':
105
+ def list_models(self) -> "typing.Dict[str, object]":
112
106
  request = urllib.request.Request(
113
107
  self._config.outcomming_models_url(),
114
108
  headers=self._build_headers(accept="application/json"),
@@ -118,8 +112,8 @@ class StreamRouter:
118
112
 
119
113
  def build_outcomming_request(
120
114
  self,
121
- incomming_request: 'typing.Dict[str, object]',
122
- ) -> 'typing.Dict[str, object]':
115
+ incomming_request: "typing.Dict[str, object]",
116
+ ) -> "typing.Dict[str, object]":
123
117
  model = str(incomming_request.get("model", "")).strip()
124
118
  if not model:
125
119
  raise UnsupportedIncommingFeature("incomming request is missing `model`")
@@ -135,7 +129,7 @@ class StreamRouter:
135
129
  if not isinstance(input_items, list):
136
130
  raise UnsupportedIncommingFeature("incomming `input` must be a list")
137
131
 
138
- payload: 'typing.Dict[str, object]' = {
132
+ payload: "typing.Dict[str, object]" = {
139
133
  "model": model,
140
134
  "messages": self._responses_input_to_chat_messages(
141
135
  instructions,
@@ -180,7 +174,7 @@ class StreamRouter:
180
174
 
181
175
  return payload
182
176
 
183
- def open_outcomming_stream(self, outcomming_request: 'typing.Dict[str, object]'):
177
+ def open_outcomming_stream(self, outcomming_request: "typing.Dict[str, object]"):
184
178
  outcomming_api = self._config.normalized_outcomming_api()
185
179
  if outcomming_api == "messages":
186
180
  return self._open_outcomming_messages_stream(outcomming_request)
@@ -192,7 +186,7 @@ class StreamRouter:
192
186
 
193
187
  def _open_outcomming_chat_stream(
194
188
  self,
195
- outcomming_request: 'typing.Dict[str, object]',
189
+ outcomming_request: "typing.Dict[str, object]",
196
190
  ):
197
191
  request = urllib.request.Request(
198
192
  self._config.outcomming_chat_completions_url(),
@@ -214,7 +208,13 @@ class StreamRouter:
214
208
  if data == "[DONE]":
215
209
  saw_done = True
216
210
  break
217
- yield json.loads(data)
211
+ payload = json.loads(data)
212
+ if isinstance(payload, dict) and payload.get("error") is not None:
213
+ raise OutcommingChatError(
214
+ "outcomming chat stream returned an error: "
215
+ + json.dumps(payload["error"], ensure_ascii=False)[:500]
216
+ )
217
+ yield payload
218
218
  if not saw_done:
219
219
  raise OutcommingChatError(
220
220
  "outcomming chat stream ended before [DONE]"
@@ -247,7 +247,7 @@ class StreamRouter:
247
247
 
248
248
  def _open_outcomming_messages_stream(
249
249
  self,
250
- outcomming_request: 'typing.Dict[str, object]',
250
+ outcomming_request: "typing.Dict[str, object]",
251
251
  ):
252
252
  try:
253
253
  messages_request = build_messages_request(outcomming_request)
@@ -267,7 +267,7 @@ class StreamRouter:
267
267
  timeout=self._config.timeout_seconds,
268
268
  ) as response:
269
269
  try:
270
- stream_state: 'typing.Dict[str, object]' = {}
270
+ stream_state: "typing.Dict[str, object]" = {}
271
271
  for event_name, data in self._iter_sse_events(response):
272
272
  if not data:
273
273
  continue
@@ -313,10 +313,10 @@ class StreamRouter:
313
313
 
314
314
  def route_stream(
315
315
  self,
316
- stored_response: 'StoredResponse',
317
- outcomming_request: 'typing.Dict[str, object]',
318
- custom_tool_names: 'typing.Union[typing.Set[str], None]' = None,
319
- trajectory_dump: 'typing.Union[TrajectoryDumpWriter, None]' = None,
316
+ stored_response: "StoredResponse",
317
+ outcomming_request: "typing.Dict[str, object]",
318
+ custom_tool_names: "typing.Union[typing.Set[str], None]" = None,
319
+ trajectory_dump: "typing.Union[TrajectoryDumpWriter, None]" = None,
320
320
  ):
321
321
  yield (
322
322
  "response.created",
@@ -331,9 +331,9 @@ class StreamRouter:
331
331
  },
332
332
  )
333
333
 
334
- text_parts: 'typing.List[str]' = []
335
- reasoning_parts: 'typing.List[str]' = []
336
- latest_usage: 'typing.Dict[str, object]' = {}
334
+ text_parts: "typing.List[str]" = []
335
+ reasoning_parts: "typing.List[str]" = []
336
+ latest_usage: "typing.Dict[str, object]" = {}
337
337
  current_request = json.loads(json.dumps(outcomming_request))
338
338
  current_stream = self._open_tracked_outcomming_stream(
339
339
  current_request,
@@ -342,9 +342,9 @@ class StreamRouter:
342
342
  retried_reasoning_only_output = False
343
343
 
344
344
  while True:
345
- tool_calls: 'typing.Dict[int, typing.Dict[str, object]]' = {}
346
- finish_reasons: 'typing.List[str]' = []
347
- current_usage: 'typing.Dict[str, object]' = {}
345
+ tool_calls: "typing.Dict[int, typing.Dict[str, object]]" = {}
346
+ finish_reasons: "typing.List[str]" = []
347
+ current_usage: "typing.Dict[str, object]" = {}
348
348
  reasoning_start = len(reasoning_parts)
349
349
  text_start = len(text_parts)
350
350
  for chunk in current_stream:
@@ -458,8 +458,8 @@ class StreamRouter:
458
458
 
459
459
  def _open_tracked_outcomming_stream(
460
460
  self,
461
- outcomming_request: 'typing.Dict[str, object]',
462
- trajectory_dump: 'typing.Union[TrajectoryDumpWriter, None]' = None,
461
+ outcomming_request: "typing.Dict[str, object]",
462
+ trajectory_dump: "typing.Union[TrajectoryDumpWriter, None]" = None,
463
463
  ):
464
464
  outcomming_stream = self.open_outcomming_stream(outcomming_request)
465
465
  if trajectory_dump is None:
@@ -471,17 +471,17 @@ class StreamRouter:
471
471
 
472
472
  def _responses_input_to_chat_messages(
473
473
  self,
474
- instructions: 'str',
475
- input_items: 'typing.List[object]',
476
- ) -> 'typing.List[typing.Dict[str, object]]':
477
- messages: 'typing.List[typing.Dict[str, object]]' = []
474
+ instructions: "str",
475
+ input_items: "typing.List[object]",
476
+ ) -> "typing.List[typing.Dict[str, object]]":
477
+ messages: "typing.List[typing.Dict[str, object]]" = []
478
478
  if instructions:
479
479
  messages.append({"role": "developer", "content": instructions})
480
480
 
481
- pending_assistant: 'typing.Union[typing.Dict[str, object], None]' = None
482
- pending_tool_images: 'typing.List[typing.Dict[str, object]]' = []
481
+ pending_assistant: "typing.Union[typing.Dict[str, object], None]" = None
482
+ pending_tool_images: "typing.List[typing.Dict[str, object]]" = []
483
483
 
484
- def flush_pending_assistant() -> 'None':
484
+ def flush_pending_assistant() -> "None":
485
485
  nonlocal pending_assistant
486
486
  if pending_assistant is None:
487
487
  return
@@ -495,7 +495,7 @@ class StreamRouter:
495
495
  messages.append(pending_assistant)
496
496
  pending_assistant = None
497
497
 
498
- def flush_pending_tool_images() -> 'None':
498
+ def flush_pending_tool_images() -> "None":
499
499
  if not pending_tool_images:
500
500
  return
501
501
  messages.append({"role": "user", "content": list(pending_tool_images)})
@@ -531,9 +531,9 @@ class StreamRouter:
531
531
  continue
532
532
  flush_pending_assistant()
533
533
  if image_parts:
534
- content: 'object' = (
535
- ([{"type": "text", "text": text}] if text else []) + image_parts
536
- )
534
+ content: "object" = (
535
+ [{"type": "text", "text": text}] if text else []
536
+ ) + image_parts
537
537
  else:
538
538
  content = text
539
539
  messages.append({"role": role, "content": content})
@@ -625,7 +625,7 @@ class StreamRouter:
625
625
  flush_pending_tool_images()
626
626
  return messages
627
627
 
628
- def _coerce_positive_int(self, raw_value: 'object') -> 'typing.Union[int, None]':
628
+ def _coerce_positive_int(self, raw_value: "object") -> "typing.Union[int, None]":
629
629
  if isinstance(raw_value, bool):
630
630
  return None
631
631
  if isinstance(raw_value, int) and raw_value > 0:
@@ -634,8 +634,8 @@ class StreamRouter:
634
634
 
635
635
  def _split_content_parts(
636
636
  self,
637
- raw_content: 'object',
638
- ) -> 'typing.Tuple[str, typing.List[typing.Dict[str, object]]]':
637
+ raw_content: "object",
638
+ ) -> "typing.Tuple[str, typing.List[typing.Dict[str, object]]]":
639
639
  if raw_content is None:
640
640
  return "", []
641
641
  if isinstance(raw_content, str):
@@ -645,8 +645,8 @@ class StreamRouter:
645
645
  "message `content` must be a list or string"
646
646
  )
647
647
 
648
- text_parts: 'typing.List[str]' = []
649
- image_parts: 'typing.List[typing.Dict[str, object]]' = []
648
+ text_parts: "typing.List[str]" = []
649
+ image_parts: "typing.List[typing.Dict[str, object]]" = []
650
650
  for part in raw_content:
651
651
  if not isinstance(part, dict):
652
652
  raise UnsupportedIncommingFeature(
@@ -666,14 +666,14 @@ class StreamRouter:
666
666
 
667
667
  def _build_chat_image_part(
668
668
  self,
669
- part: 'typing.Dict[str, object]',
670
- ) -> 'typing.Dict[str, object]':
669
+ part: "typing.Dict[str, object]",
670
+ ) -> "typing.Dict[str, object]":
671
671
  image_url = str(part.get("image_url", "") or "").strip()
672
672
  if not image_url:
673
673
  raise UnsupportedIncommingFeature(
674
674
  "`input_image` content parts must carry a non-empty `image_url`"
675
675
  )
676
- image_payload: 'typing.Dict[str, object]' = {"url": image_url}
676
+ image_payload: "typing.Dict[str, object]" = {"url": image_url}
677
677
  detail = part.get("detail")
678
678
  if isinstance(detail, str) and detail in {"auto", "low", "high"}:
679
679
  image_payload["detail"] = detail
@@ -681,18 +681,18 @@ class StreamRouter:
681
681
 
682
682
  def _split_tool_output_parts(
683
683
  self,
684
- raw_output: 'object',
685
- ) -> 'typing.Tuple[str, typing.List[typing.Dict[str, object]]]':
684
+ raw_output: "object",
685
+ ) -> "typing.Tuple[str, typing.List[typing.Dict[str, object]]]":
686
686
  if isinstance(raw_output, str):
687
687
  return raw_output, []
688
688
  if isinstance(raw_output, list):
689
689
  return self._split_content_parts(raw_output)
690
690
  return json.dumps(raw_output, ensure_ascii=False), []
691
691
 
692
- def _coalesce_reasoning_text(self, raw_item: 'typing.Dict[str, object]') -> 'str':
692
+ def _coalesce_reasoning_text(self, raw_item: "typing.Dict[str, object]") -> "str":
693
693
  content = raw_item.get("content")
694
694
  if isinstance(content, list):
695
- text_parts: 'typing.List[str]' = []
695
+ text_parts: "typing.List[str]" = []
696
696
  for part in content:
697
697
  if not isinstance(part, dict):
698
698
  continue
@@ -719,8 +719,10 @@ class StreamRouter:
719
719
  return value
720
720
  return ""
721
721
 
722
- def _translate_tools(self, incomming_tools: 'typing.List[object]') -> 'typing.List[typing.Dict[str, object]]':
723
- translated: 'typing.List[typing.Dict[str, object]]' = []
722
+ def _translate_tools(
723
+ self, incomming_tools: "typing.List[object]"
724
+ ) -> "typing.List[typing.Dict[str, object]]":
725
+ translated: "typing.List[typing.Dict[str, object]]" = []
724
726
  for raw_tool in incomming_tools:
725
727
  if not isinstance(raw_tool, dict):
726
728
  raise UnsupportedIncommingFeature("tool definitions must be objects")
@@ -755,7 +757,7 @@ class StreamRouter:
755
757
  )
756
758
  return translated
757
759
 
758
- def _translate_tool_choice(self, raw_tool_choice: 'object') -> 'object':
760
+ def _translate_tool_choice(self, raw_tool_choice: "object") -> "object":
759
761
  if isinstance(raw_tool_choice, str):
760
762
  return raw_tool_choice
761
763
  if not isinstance(raw_tool_choice, dict):
@@ -778,14 +780,14 @@ class StreamRouter:
778
780
 
779
781
  def _consume_chat_chunk(
780
782
  self,
781
- payload: 'typing.Dict[str, object]',
782
- reasoning_parts: 'typing.List[str]',
783
- text_parts: 'typing.List[str]',
784
- tool_calls: 'typing.Dict[int, typing.Dict[str, object]]',
785
- current_usage: 'typing.Dict[str, object]',
786
- finish_reasons: 'typing.List[str]',
787
- ) -> 'typing.List[typing.Tuple[str, typing.Dict[str, object]]]':
788
- events: 'typing.List[typing.Tuple[str, typing.Dict[str, object]]]' = []
783
+ payload: "typing.Dict[str, object]",
784
+ reasoning_parts: "typing.List[str]",
785
+ text_parts: "typing.List[str]",
786
+ tool_calls: "typing.Dict[int, typing.Dict[str, object]]",
787
+ current_usage: "typing.Dict[str, object]",
788
+ finish_reasons: "typing.List[str]",
789
+ ) -> "typing.List[typing.Tuple[str, typing.Dict[str, object]]]":
790
+ events: "typing.List[typing.Tuple[str, typing.Dict[str, object]]]" = []
789
791
  usage = payload.get("usage")
790
792
  if isinstance(usage, dict):
791
793
  self._capture_usage_snapshot(current_usage, usage)
@@ -876,9 +878,9 @@ class StreamRouter:
876
878
 
877
879
  def _capture_usage_snapshot(
878
880
  self,
879
- current_usage: 'typing.Dict[str, object]',
880
- usage: 'typing.Dict[str, object]',
881
- ) -> 'None':
881
+ current_usage: "typing.Dict[str, object]",
882
+ usage: "typing.Dict[str, object]",
883
+ ) -> "None":
882
884
  scalar_mappings = (
883
885
  ("input_tokens", usage.get("input_tokens", usage.get("prompt_tokens"))),
884
886
  (
@@ -910,12 +912,12 @@ class StreamRouter:
910
912
 
911
913
  def _build_output_items(
912
914
  self,
913
- reasoning_parts: 'typing.List[str]',
914
- text_parts: 'typing.List[str]',
915
- tool_calls: 'typing.Dict[int, typing.Dict[str, object]]',
916
- custom_tool_names: 'typing.Set[str]',
917
- ) -> 'typing.List[typing.Dict[str, object]]':
918
- items: 'typing.List[typing.Dict[str, object]]' = []
915
+ reasoning_parts: "typing.List[str]",
916
+ text_parts: "typing.List[str]",
917
+ tool_calls: "typing.Dict[int, typing.Dict[str, object]]",
918
+ custom_tool_names: "typing.Set[str]",
919
+ ) -> "typing.List[typing.Dict[str, object]]":
920
+ items: "typing.List[typing.Dict[str, object]]" = []
919
921
  reasoning_text = "".join(reasoning_parts)
920
922
  if reasoning_text:
921
923
  items.append(
@@ -971,8 +973,7 @@ class StreamRouter:
971
973
  items.append(
972
974
  {
973
975
  "type": "function_call",
974
- "call_id": str(tool_call.get("id", "")).strip()
975
- or f"call_{index}",
976
+ "call_id": str(tool_call.get("id", "")).strip() or f"call_{index}",
976
977
  "name": name,
977
978
  "arguments": arguments,
978
979
  }
@@ -980,7 +981,9 @@ class StreamRouter:
980
981
 
981
982
  return items
982
983
 
983
- def _request_json(self, request: 'urllib.request.Request') -> 'typing.Dict[str, object]':
984
+ def _request_json(
985
+ self, request: "urllib.request.Request"
986
+ ) -> "typing.Dict[str, object]":
984
987
  try:
985
988
  with urllib.request.urlopen(
986
989
  request,
@@ -1003,7 +1006,7 @@ class StreamRouter:
1003
1006
  f"outcomming request failed: {exc.reason}"
1004
1007
  ) from exc
1005
1008
 
1006
- def _build_headers(self, accept: 'str') -> 'typing.Dict[str, str]':
1009
+ def _build_headers(self, accept: "str") -> "typing.Dict[str, str]":
1007
1010
  headers = {
1008
1011
  "Accept": accept,
1009
1012
  "Content-Type": "application/json",
@@ -1014,8 +1017,8 @@ class StreamRouter:
1014
1017
  return headers
1015
1018
 
1016
1019
  def _iter_sse_events(self, response):
1017
- event_name: 'typing.Union[str, None]' = None
1018
- data_lines: 'typing.List[str]' = []
1020
+ event_name: "typing.Union[str, None]" = None
1021
+ data_lines: "typing.List[str]" = []
1019
1022
 
1020
1023
  for raw_line in response:
1021
1024
  line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")