python-codex 0.2.7__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycodex/__init__.py +14 -14
- pycodex/agent.py +465 -499
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -510
- pycodex/compat.py +19 -5
- pycodex/context.py +222 -212
- pycodex/doctor.py +52 -48
- pycodex/events.py +857 -0
- pycodex/feishu_card.py +217 -163
- pycodex/feishu_link.py +43 -83
- pycodex/model.py +324 -253
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +76 -45
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +321 -883
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -176
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +1 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +58 -25
- pycodex/tools/close_agent_tool.py +2 -2
- pycodex/tools/code_mode_manager.py +77 -64
- pycodex/tools/exec_command_tool.py +26 -11
- pycodex/tools/exec_tool.py +4 -4
- pycodex/tools/grep_files_tool.py +12 -10
- pycodex/tools/ipython_tool.py +10 -13
- pycodex/tools/list_dir_tool.py +13 -9
- pycodex/tools/read_file_tool.py +29 -17
- pycodex/tools/request_permissions_tool.py +15 -5
- pycodex/tools/request_user_input_tool.py +13 -104
- pycodex/tools/resume_agent_tool.py +2 -2
- pycodex/tools/send_input_tool.py +11 -8
- pycodex/tools/shell_command_tool.py +7 -5
- pycodex/tools/shell_tool.py +7 -5
- pycodex/tools/spawn_agent_tool.py +7 -4
- pycodex/tools/unified_exec_manager.py +102 -69
- pycodex/tools/update_plan_tool.py +8 -5
- pycodex/tools/view_image_tool.py +7 -5
- pycodex/tools/wait_agent_tool.py +27 -4
- pycodex/tools/wait_tool.py +5 -4
- pycodex/tools/web_search_tool.py +4 -2
- pycodex/tools/write_stdin_tool.py +12 -11
- pycodex/utils/__init__.py +2 -17
- pycodex/utils/compactor.py +41 -72
- pycodex/utils/debug.py +2 -2
- pycodex/utils/dotenv.py +6 -7
- pycodex/utils/event_helpers.py +190 -0
- pycodex/utils/get_env.py +27 -70
- pycodex/{image_utils.py → utils/image_utils.py} +8 -11
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +217 -163
- pycodex/utils/truncation.py +21 -45
- python_codex-0.3.1.dist-info/METADATA +743 -0
- python_codex-0.3.1.dist-info/RECORD +91 -0
- responses_server/__init__.py +1 -5
- responses_server/__main__.py +0 -1
- responses_server/app.py +34 -33
- responses_server/config.py +23 -23
- responses_server/messages_api.py +51 -53
- responses_server/payload_processors.py +25 -20
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +209 -98
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +36 -14
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +465 -377
- workspace_server/owned_process.py +27 -0
- workspace_server/workspace.html +854 -228
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +141 -84
- pycodex/collaboration.py +0 -20
- pycodex/interactive_session.py +0 -415
- pycodex/prompts/collaboration_default.md +0 -11
- pycodex/prompts/collaboration_plan.md +0 -128
- pycodex/utils/toolcall_visualize.py +0 -713
- pycodex/utils/visualize.py +0 -560
- python_codex-0.2.7.dist-info/METADATA +0 -455
- python_codex-0.2.7.dist-info/RECORD +0 -93
- {python_codex-0.2.7.dist-info → python_codex-0.3.1.dist-info}/WHEEL +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.1.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
import json
|
|
3
1
|
import http.client
|
|
2
|
+
import json
|
|
4
3
|
import ssl
|
|
4
|
+
import time
|
|
5
|
+
import typing
|
|
5
6
|
import urllib.error
|
|
6
7
|
import urllib.request
|
|
8
|
+
from contextlib import contextmanager
|
|
7
9
|
|
|
8
10
|
from .config import CompatServerConfig
|
|
9
|
-
from .messages_api import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
iter_chat_chunks as iter_chat_chunks_from_messages,
|
|
13
|
-
saw_message_stop as messages_saw_message_stop,
|
|
14
|
-
)
|
|
11
|
+
from .messages_api import MessagesAPIAdapterError, build_messages_request
|
|
12
|
+
from .messages_api import iter_chat_chunks as iter_chat_chunks_from_messages
|
|
13
|
+
from .messages_api import saw_message_stop as messages_saw_message_stop
|
|
15
14
|
from .session_store import StoredResponse
|
|
16
15
|
from .tools import WebSearchTool, collect_custom_tool_names
|
|
17
|
-
from .tools.custom_adapter import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
build_tool_definition as build_custom_tool_definition,
|
|
22
|
-
)
|
|
16
|
+
from .tools.custom_adapter import CustomToolAdapterError
|
|
17
|
+
from .tools.custom_adapter import build_output_item as build_custom_output_item
|
|
18
|
+
from .tools.custom_adapter import build_tool_call as build_custom_tool_call
|
|
19
|
+
from .tools.custom_adapter import build_tool_definition as build_custom_tool_definition
|
|
23
20
|
from .tools.web_search import (
|
|
24
21
|
build_followup_request,
|
|
25
22
|
build_output_items,
|
|
@@ -28,7 +25,6 @@ from .tools.web_search import (
|
|
|
28
25
|
partition_tool_calls,
|
|
29
26
|
)
|
|
30
27
|
from .trajectory_dump import TrajectoryDumpWriter
|
|
31
|
-
import typing
|
|
32
28
|
|
|
33
29
|
|
|
34
30
|
class UnsupportedIncommingFeature(ValueError):
|
|
@@ -38,23 +34,23 @@ class UnsupportedIncommingFeature(ValueError):
|
|
|
38
34
|
class OutcommingChatError(RuntimeError):
|
|
39
35
|
def __init__(
|
|
40
36
|
self,
|
|
41
|
-
message:
|
|
42
|
-
error_type:
|
|
43
|
-
) ->
|
|
37
|
+
message: "str",
|
|
38
|
+
error_type: "typing.Union[str, None]" = None,
|
|
39
|
+
) -> "None":
|
|
44
40
|
super().__init__(message)
|
|
45
41
|
self.error_type = error_type
|
|
46
42
|
|
|
47
43
|
|
|
48
44
|
class StreamRouter:
|
|
49
|
-
def __init__(self, config:
|
|
45
|
+
def __init__(self, config: "CompatServerConfig") -> "None":
|
|
50
46
|
self._config = config
|
|
51
47
|
self._mock_web_search = WebSearchTool()
|
|
52
48
|
|
|
53
49
|
def _provider_capability(
|
|
54
50
|
self,
|
|
55
|
-
explicit_support:
|
|
56
|
-
default:
|
|
57
|
-
) ->
|
|
51
|
+
explicit_support: "typing.Dict[str, bool]",
|
|
52
|
+
default: "typing.Union[bool, None]" = None,
|
|
53
|
+
) -> "bool":
|
|
58
54
|
provider_name = str(self._config.model_provider or "").strip().lower()
|
|
59
55
|
if provider_name in explicit_support:
|
|
60
56
|
return explicit_support[provider_name]
|
|
@@ -64,7 +60,7 @@ class StreamRouter:
|
|
|
64
60
|
return default
|
|
65
61
|
raise KeyError("provider capability map is missing `vllm` fallback")
|
|
66
62
|
|
|
67
|
-
def _supports_chat_reasoning(self) ->
|
|
63
|
+
def _supports_chat_reasoning(self) -> "bool":
|
|
68
64
|
# Unknown providers inherit the vLLM compatibility behavior unless a
|
|
69
65
|
# provider is explicitly declared otherwise.
|
|
70
66
|
return self._provider_capability(
|
|
@@ -74,7 +70,7 @@ class StreamRouter:
|
|
|
74
70
|
}
|
|
75
71
|
)
|
|
76
72
|
|
|
77
|
-
def _supports_stream_usage(self) ->
|
|
73
|
+
def _supports_stream_usage(self) -> "bool":
|
|
78
74
|
return self._provider_capability(
|
|
79
75
|
{
|
|
80
76
|
"vllm": True,
|
|
@@ -84,8 +80,8 @@ class StreamRouter:
|
|
|
84
80
|
|
|
85
81
|
def validate_incomming_request(
|
|
86
82
|
self,
|
|
87
|
-
incomming_request:
|
|
88
|
-
) ->
|
|
83
|
+
incomming_request: "typing.Dict[str, object]",
|
|
84
|
+
) -> "None":
|
|
89
85
|
model = str(incomming_request.get("model", "")).strip()
|
|
90
86
|
if not model:
|
|
91
87
|
raise UnsupportedIncommingFeature("incomming request is missing `model`")
|
|
@@ -104,11 +100,11 @@ class StreamRouter:
|
|
|
104
100
|
|
|
105
101
|
def collect_custom_tool_names(
|
|
106
102
|
self,
|
|
107
|
-
incomming_request:
|
|
108
|
-
) ->
|
|
103
|
+
incomming_request: "typing.Dict[str, object]",
|
|
104
|
+
) -> "typing.Set[str]":
|
|
109
105
|
return collect_custom_tool_names(incomming_request.get("tools") or [])
|
|
110
106
|
|
|
111
|
-
def list_models(self) ->
|
|
107
|
+
def list_models(self) -> "typing.Dict[str, object]":
|
|
112
108
|
request = urllib.request.Request(
|
|
113
109
|
self._config.outcomming_models_url(),
|
|
114
110
|
headers=self._build_headers(accept="application/json"),
|
|
@@ -118,8 +114,8 @@ class StreamRouter:
|
|
|
118
114
|
|
|
119
115
|
def build_outcomming_request(
|
|
120
116
|
self,
|
|
121
|
-
incomming_request:
|
|
122
|
-
) ->
|
|
117
|
+
incomming_request: "typing.Dict[str, object]",
|
|
118
|
+
) -> "typing.Dict[str, object]":
|
|
123
119
|
model = str(incomming_request.get("model", "")).strip()
|
|
124
120
|
if not model:
|
|
125
121
|
raise UnsupportedIncommingFeature("incomming request is missing `model`")
|
|
@@ -135,7 +131,7 @@ class StreamRouter:
|
|
|
135
131
|
if not isinstance(input_items, list):
|
|
136
132
|
raise UnsupportedIncommingFeature("incomming `input` must be a list")
|
|
137
133
|
|
|
138
|
-
payload:
|
|
134
|
+
payload: "typing.Dict[str, object]" = {
|
|
139
135
|
"model": model,
|
|
140
136
|
"messages": self._responses_input_to_chat_messages(
|
|
141
137
|
instructions,
|
|
@@ -180,7 +176,7 @@ class StreamRouter:
|
|
|
180
176
|
|
|
181
177
|
return payload
|
|
182
178
|
|
|
183
|
-
def open_outcomming_stream(self, outcomming_request:
|
|
179
|
+
def open_outcomming_stream(self, outcomming_request: "typing.Dict[str, object]"):
|
|
184
180
|
outcomming_api = self._config.normalized_outcomming_api()
|
|
185
181
|
if outcomming_api == "messages":
|
|
186
182
|
return self._open_outcomming_messages_stream(outcomming_request)
|
|
@@ -192,7 +188,7 @@ class StreamRouter:
|
|
|
192
188
|
|
|
193
189
|
def _open_outcomming_chat_stream(
|
|
194
190
|
self,
|
|
195
|
-
outcomming_request:
|
|
191
|
+
outcomming_request: "typing.Dict[str, object]",
|
|
196
192
|
):
|
|
197
193
|
request = urllib.request.Request(
|
|
198
194
|
self._config.outcomming_chat_completions_url(),
|
|
@@ -200,12 +196,19 @@ class StreamRouter:
|
|
|
200
196
|
headers=self._build_headers(accept="text/event-stream"),
|
|
201
197
|
method="POST",
|
|
202
198
|
)
|
|
199
|
+
with self._request_diagnostics(request) as progress:
|
|
200
|
+
yield from self._read_outcomming_chat_stream(request, progress)
|
|
201
|
+
|
|
202
|
+
def _read_outcomming_chat_stream(self, request, progress):
|
|
203
203
|
try:
|
|
204
204
|
with urllib.request.urlopen(
|
|
205
205
|
request,
|
|
206
206
|
context=ssl.create_default_context(),
|
|
207
207
|
timeout=self._config.timeout_seconds,
|
|
208
208
|
) as response:
|
|
209
|
+
progress["headers_at"] = time.monotonic()
|
|
210
|
+
progress["status"] = response.status
|
|
211
|
+
progress["phase"] = "waiting_first_chunk"
|
|
209
212
|
try:
|
|
210
213
|
saw_done = False
|
|
211
214
|
for _event_name, data in self._iter_sse_events(response):
|
|
@@ -214,7 +217,14 @@ class StreamRouter:
|
|
|
214
217
|
if data == "[DONE]":
|
|
215
218
|
saw_done = True
|
|
216
219
|
break
|
|
217
|
-
|
|
220
|
+
payload = json.loads(data)
|
|
221
|
+
self._received_chunk(progress)
|
|
222
|
+
if isinstance(payload, dict) and payload.get("error") is not None:
|
|
223
|
+
raise OutcommingChatError(
|
|
224
|
+
"outcomming chat stream returned an error: "
|
|
225
|
+
+ json.dumps(payload["error"], ensure_ascii=False)[:500]
|
|
226
|
+
)
|
|
227
|
+
yield payload
|
|
218
228
|
if not saw_done:
|
|
219
229
|
raise OutcommingChatError(
|
|
220
230
|
"outcomming chat stream ended before [DONE]"
|
|
@@ -231,6 +241,9 @@ class StreamRouter:
|
|
|
231
241
|
f"{exc}"
|
|
232
242
|
) from exc
|
|
233
243
|
except urllib.error.HTTPError as exc:
|
|
244
|
+
progress["headers_at"] = time.monotonic()
|
|
245
|
+
progress["status"] = exc.code
|
|
246
|
+
progress["phase"] = "reading_error_body"
|
|
234
247
|
body = exc.read().decode("utf-8", errors="replace")
|
|
235
248
|
raise OutcommingChatError(
|
|
236
249
|
f"outcomming chat request failed with status {exc.code}: {body[:500]}"
|
|
@@ -244,10 +257,15 @@ class StreamRouter:
|
|
|
244
257
|
raise OutcommingChatError(
|
|
245
258
|
f"outcomming chat request failed: {exc.reason}"
|
|
246
259
|
) from exc
|
|
260
|
+
except (OSError, EOFError, http.client.HTTPException) as exc:
|
|
261
|
+
raise OutcommingChatError(
|
|
262
|
+
"outcomming chat request failed while opening response: "
|
|
263
|
+
f"{type(exc).__name__}: {exc}"
|
|
264
|
+
) from exc
|
|
247
265
|
|
|
248
266
|
def _open_outcomming_messages_stream(
|
|
249
267
|
self,
|
|
250
|
-
outcomming_request:
|
|
268
|
+
outcomming_request: "typing.Dict[str, object]",
|
|
251
269
|
):
|
|
252
270
|
try:
|
|
253
271
|
messages_request = build_messages_request(outcomming_request)
|
|
@@ -260,20 +278,28 @@ class StreamRouter:
|
|
|
260
278
|
headers=self._build_headers(accept="text/event-stream"),
|
|
261
279
|
method="POST",
|
|
262
280
|
)
|
|
281
|
+
with self._request_diagnostics(request) as progress:
|
|
282
|
+
yield from self._read_outcomming_messages_stream(request, progress)
|
|
283
|
+
|
|
284
|
+
def _read_outcomming_messages_stream(self, request, progress):
|
|
263
285
|
try:
|
|
264
286
|
with urllib.request.urlopen(
|
|
265
287
|
request,
|
|
266
288
|
context=ssl.create_default_context(),
|
|
267
289
|
timeout=self._config.timeout_seconds,
|
|
268
290
|
) as response:
|
|
291
|
+
progress["headers_at"] = time.monotonic()
|
|
292
|
+
progress["status"] = response.status
|
|
293
|
+
progress["phase"] = "waiting_first_chunk"
|
|
269
294
|
try:
|
|
270
|
-
stream_state:
|
|
295
|
+
stream_state: "typing.Dict[str, object]" = {}
|
|
271
296
|
for event_name, data in self._iter_sse_events(response):
|
|
272
297
|
if not data:
|
|
273
298
|
continue
|
|
274
299
|
payload = json.loads(data)
|
|
275
300
|
if not isinstance(payload, dict):
|
|
276
301
|
continue
|
|
302
|
+
self._received_chunk(progress)
|
|
277
303
|
for chunk in iter_chat_chunks_from_messages(
|
|
278
304
|
event_name,
|
|
279
305
|
payload,
|
|
@@ -297,6 +323,9 @@ class StreamRouter:
|
|
|
297
323
|
f"{exc}"
|
|
298
324
|
) from exc
|
|
299
325
|
except urllib.error.HTTPError as exc:
|
|
326
|
+
progress["headers_at"] = time.monotonic()
|
|
327
|
+
progress["status"] = exc.code
|
|
328
|
+
progress["phase"] = "reading_error_body"
|
|
300
329
|
body = exc.read().decode("utf-8", errors="replace")
|
|
301
330
|
raise OutcommingChatError(
|
|
302
331
|
f"outcomming messages request failed with status {exc.code}: {body[:500]}"
|
|
@@ -310,13 +339,18 @@ class StreamRouter:
|
|
|
310
339
|
raise OutcommingChatError(
|
|
311
340
|
f"outcomming messages request failed: {exc.reason}"
|
|
312
341
|
) from exc
|
|
342
|
+
except (OSError, EOFError, http.client.HTTPException) as exc:
|
|
343
|
+
raise OutcommingChatError(
|
|
344
|
+
"outcomming messages request failed while opening response: "
|
|
345
|
+
f"{type(exc).__name__}: {exc}"
|
|
346
|
+
) from exc
|
|
313
347
|
|
|
314
348
|
def route_stream(
|
|
315
349
|
self,
|
|
316
|
-
stored_response:
|
|
317
|
-
outcomming_request:
|
|
318
|
-
custom_tool_names:
|
|
319
|
-
trajectory_dump:
|
|
350
|
+
stored_response: "StoredResponse",
|
|
351
|
+
outcomming_request: "typing.Dict[str, object]",
|
|
352
|
+
custom_tool_names: "typing.Union[typing.Set[str], None]" = None,
|
|
353
|
+
trajectory_dump: "typing.Union[TrajectoryDumpWriter, None]" = None,
|
|
320
354
|
):
|
|
321
355
|
yield (
|
|
322
356
|
"response.created",
|
|
@@ -331,9 +365,9 @@ class StreamRouter:
|
|
|
331
365
|
},
|
|
332
366
|
)
|
|
333
367
|
|
|
334
|
-
text_parts:
|
|
335
|
-
reasoning_parts:
|
|
336
|
-
latest_usage:
|
|
368
|
+
text_parts: "typing.List[str]" = []
|
|
369
|
+
reasoning_parts: "typing.List[str]" = []
|
|
370
|
+
latest_usage: "typing.Dict[str, object]" = {}
|
|
337
371
|
current_request = json.loads(json.dumps(outcomming_request))
|
|
338
372
|
current_stream = self._open_tracked_outcomming_stream(
|
|
339
373
|
current_request,
|
|
@@ -342,9 +376,9 @@ class StreamRouter:
|
|
|
342
376
|
retried_reasoning_only_output = False
|
|
343
377
|
|
|
344
378
|
while True:
|
|
345
|
-
tool_calls:
|
|
346
|
-
finish_reasons:
|
|
347
|
-
current_usage:
|
|
379
|
+
tool_calls: "typing.Dict[int, typing.Dict[str, object]]" = {}
|
|
380
|
+
finish_reasons: "typing.List[str]" = []
|
|
381
|
+
current_usage: "typing.Dict[str, object]" = {}
|
|
348
382
|
reasoning_start = len(reasoning_parts)
|
|
349
383
|
text_start = len(text_parts)
|
|
350
384
|
for chunk in current_stream:
|
|
@@ -458,8 +492,8 @@ class StreamRouter:
|
|
|
458
492
|
|
|
459
493
|
def _open_tracked_outcomming_stream(
|
|
460
494
|
self,
|
|
461
|
-
outcomming_request:
|
|
462
|
-
trajectory_dump:
|
|
495
|
+
outcomming_request: "typing.Dict[str, object]",
|
|
496
|
+
trajectory_dump: "typing.Union[TrajectoryDumpWriter, None]" = None,
|
|
463
497
|
):
|
|
464
498
|
outcomming_stream = self.open_outcomming_stream(outcomming_request)
|
|
465
499
|
if trajectory_dump is None:
|
|
@@ -469,19 +503,71 @@ class StreamRouter:
|
|
|
469
503
|
outcomming_request,
|
|
470
504
|
)
|
|
471
505
|
|
|
506
|
+
@contextmanager
|
|
507
|
+
def _request_diagnostics(self, request):
|
|
508
|
+
started = time.monotonic()
|
|
509
|
+
progress = {
|
|
510
|
+
"phase": "waiting_response_headers",
|
|
511
|
+
"headers_at": None,
|
|
512
|
+
"first_chunk_at": None,
|
|
513
|
+
"last_chunk_at": None,
|
|
514
|
+
"chunks": 0,
|
|
515
|
+
"status": None,
|
|
516
|
+
}
|
|
517
|
+
try:
|
|
518
|
+
yield progress
|
|
519
|
+
except OutcommingChatError as exc:
|
|
520
|
+
now = time.monotonic()
|
|
521
|
+
cause = exc.__cause__ or exc
|
|
522
|
+
if isinstance(cause, urllib.error.URLError) and isinstance(
|
|
523
|
+
cause.reason, BaseException
|
|
524
|
+
):
|
|
525
|
+
cause = cause.reason
|
|
526
|
+
|
|
527
|
+
def timing(received_at):
|
|
528
|
+
if received_at is None:
|
|
529
|
+
return "not_received"
|
|
530
|
+
return f"{received_at - started:.2f}s"
|
|
531
|
+
|
|
532
|
+
idle = (
|
|
533
|
+
f"{now - progress['last_chunk_at']:.2f}s"
|
|
534
|
+
if progress["last_chunk_at"] is not None
|
|
535
|
+
else "not_started"
|
|
536
|
+
)
|
|
537
|
+
details = (
|
|
538
|
+
f"phase={progress['phase']}, elapsed={now - started:.2f}s, "
|
|
539
|
+
f"timeout={self._config.timeout_seconds:g}s\n"
|
|
540
|
+
f"headers={timing(progress['headers_at'])}, "
|
|
541
|
+
f"first_chunk={timing(progress['first_chunk_at'])}, "
|
|
542
|
+
f"chunks={progress['chunks']}, idle={idle}\n"
|
|
543
|
+
f"status={progress['status']}, "
|
|
544
|
+
f"request_bytes={len(request.data or b'')}, "
|
|
545
|
+
f"exception={type(cause).__name__}"
|
|
546
|
+
)
|
|
547
|
+
exc.args = (f"{exc}\n{details}",)
|
|
548
|
+
raise
|
|
549
|
+
|
|
550
|
+
def _received_chunk(self, progress):
|
|
551
|
+
now = time.monotonic()
|
|
552
|
+
if progress["chunks"] == 0:
|
|
553
|
+
progress["first_chunk_at"] = now
|
|
554
|
+
progress["last_chunk_at"] = now
|
|
555
|
+
progress["chunks"] += 1
|
|
556
|
+
progress["phase"] = "reading_stream"
|
|
557
|
+
|
|
472
558
|
def _responses_input_to_chat_messages(
|
|
473
559
|
self,
|
|
474
|
-
instructions:
|
|
475
|
-
input_items:
|
|
476
|
-
) ->
|
|
477
|
-
messages:
|
|
560
|
+
instructions: "str",
|
|
561
|
+
input_items: "typing.List[object]",
|
|
562
|
+
) -> "typing.List[typing.Dict[str, object]]":
|
|
563
|
+
messages: "typing.List[typing.Dict[str, object]]" = []
|
|
478
564
|
if instructions:
|
|
479
565
|
messages.append({"role": "developer", "content": instructions})
|
|
480
566
|
|
|
481
|
-
pending_assistant:
|
|
482
|
-
pending_tool_images:
|
|
567
|
+
pending_assistant: "typing.Union[typing.Dict[str, object], None]" = None
|
|
568
|
+
pending_tool_images: "typing.List[typing.Dict[str, object]]" = []
|
|
483
569
|
|
|
484
|
-
def flush_pending_assistant() ->
|
|
570
|
+
def flush_pending_assistant() -> "None":
|
|
485
571
|
nonlocal pending_assistant
|
|
486
572
|
if pending_assistant is None:
|
|
487
573
|
return
|
|
@@ -495,7 +581,7 @@ class StreamRouter:
|
|
|
495
581
|
messages.append(pending_assistant)
|
|
496
582
|
pending_assistant = None
|
|
497
583
|
|
|
498
|
-
def flush_pending_tool_images() ->
|
|
584
|
+
def flush_pending_tool_images() -> "None":
|
|
499
585
|
if not pending_tool_images:
|
|
500
586
|
return
|
|
501
587
|
messages.append({"role": "user", "content": list(pending_tool_images)})
|
|
@@ -531,9 +617,9 @@ class StreamRouter:
|
|
|
531
617
|
continue
|
|
532
618
|
flush_pending_assistant()
|
|
533
619
|
if image_parts:
|
|
534
|
-
content:
|
|
535
|
-
|
|
536
|
-
)
|
|
620
|
+
content: "object" = (
|
|
621
|
+
[{"type": "text", "text": text}] if text else []
|
|
622
|
+
) + image_parts
|
|
537
623
|
else:
|
|
538
624
|
content = text
|
|
539
625
|
messages.append({"role": role, "content": content})
|
|
@@ -625,7 +711,7 @@ class StreamRouter:
|
|
|
625
711
|
flush_pending_tool_images()
|
|
626
712
|
return messages
|
|
627
713
|
|
|
628
|
-
def _coerce_positive_int(self, raw_value:
|
|
714
|
+
def _coerce_positive_int(self, raw_value: "object") -> "typing.Union[int, None]":
|
|
629
715
|
if isinstance(raw_value, bool):
|
|
630
716
|
return None
|
|
631
717
|
if isinstance(raw_value, int) and raw_value > 0:
|
|
@@ -634,8 +720,8 @@ class StreamRouter:
|
|
|
634
720
|
|
|
635
721
|
def _split_content_parts(
|
|
636
722
|
self,
|
|
637
|
-
raw_content:
|
|
638
|
-
) ->
|
|
723
|
+
raw_content: "object",
|
|
724
|
+
) -> "typing.Tuple[str, typing.List[typing.Dict[str, object]]]":
|
|
639
725
|
if raw_content is None:
|
|
640
726
|
return "", []
|
|
641
727
|
if isinstance(raw_content, str):
|
|
@@ -645,8 +731,8 @@ class StreamRouter:
|
|
|
645
731
|
"message `content` must be a list or string"
|
|
646
732
|
)
|
|
647
733
|
|
|
648
|
-
text_parts:
|
|
649
|
-
image_parts:
|
|
734
|
+
text_parts: "typing.List[str]" = []
|
|
735
|
+
image_parts: "typing.List[typing.Dict[str, object]]" = []
|
|
650
736
|
for part in raw_content:
|
|
651
737
|
if not isinstance(part, dict):
|
|
652
738
|
raise UnsupportedIncommingFeature(
|
|
@@ -666,14 +752,14 @@ class StreamRouter:
|
|
|
666
752
|
|
|
667
753
|
def _build_chat_image_part(
|
|
668
754
|
self,
|
|
669
|
-
part:
|
|
670
|
-
) ->
|
|
755
|
+
part: "typing.Dict[str, object]",
|
|
756
|
+
) -> "typing.Dict[str, object]":
|
|
671
757
|
image_url = str(part.get("image_url", "") or "").strip()
|
|
672
758
|
if not image_url:
|
|
673
759
|
raise UnsupportedIncommingFeature(
|
|
674
760
|
"`input_image` content parts must carry a non-empty `image_url`"
|
|
675
761
|
)
|
|
676
|
-
image_payload:
|
|
762
|
+
image_payload: "typing.Dict[str, object]" = {"url": image_url}
|
|
677
763
|
detail = part.get("detail")
|
|
678
764
|
if isinstance(detail, str) and detail in {"auto", "low", "high"}:
|
|
679
765
|
image_payload["detail"] = detail
|
|
@@ -681,18 +767,18 @@ class StreamRouter:
|
|
|
681
767
|
|
|
682
768
|
def _split_tool_output_parts(
|
|
683
769
|
self,
|
|
684
|
-
raw_output:
|
|
685
|
-
) ->
|
|
770
|
+
raw_output: "object",
|
|
771
|
+
) -> "typing.Tuple[str, typing.List[typing.Dict[str, object]]]":
|
|
686
772
|
if isinstance(raw_output, str):
|
|
687
773
|
return raw_output, []
|
|
688
774
|
if isinstance(raw_output, list):
|
|
689
775
|
return self._split_content_parts(raw_output)
|
|
690
776
|
return json.dumps(raw_output, ensure_ascii=False), []
|
|
691
777
|
|
|
692
|
-
def _coalesce_reasoning_text(self, raw_item:
|
|
778
|
+
def _coalesce_reasoning_text(self, raw_item: "typing.Dict[str, object]") -> "str":
|
|
693
779
|
content = raw_item.get("content")
|
|
694
780
|
if isinstance(content, list):
|
|
695
|
-
text_parts:
|
|
781
|
+
text_parts: "typing.List[str]" = []
|
|
696
782
|
for part in content:
|
|
697
783
|
if not isinstance(part, dict):
|
|
698
784
|
continue
|
|
@@ -719,8 +805,10 @@ class StreamRouter:
|
|
|
719
805
|
return value
|
|
720
806
|
return ""
|
|
721
807
|
|
|
722
|
-
def _translate_tools(
|
|
723
|
-
|
|
808
|
+
def _translate_tools(
|
|
809
|
+
self, incomming_tools: "typing.List[object]"
|
|
810
|
+
) -> "typing.List[typing.Dict[str, object]]":
|
|
811
|
+
translated: "typing.List[typing.Dict[str, object]]" = []
|
|
724
812
|
for raw_tool in incomming_tools:
|
|
725
813
|
if not isinstance(raw_tool, dict):
|
|
726
814
|
raise UnsupportedIncommingFeature("tool definitions must be objects")
|
|
@@ -755,7 +843,7 @@ class StreamRouter:
|
|
|
755
843
|
)
|
|
756
844
|
return translated
|
|
757
845
|
|
|
758
|
-
def _translate_tool_choice(self, raw_tool_choice:
|
|
846
|
+
def _translate_tool_choice(self, raw_tool_choice: "object") -> "object":
|
|
759
847
|
if isinstance(raw_tool_choice, str):
|
|
760
848
|
return raw_tool_choice
|
|
761
849
|
if not isinstance(raw_tool_choice, dict):
|
|
@@ -778,14 +866,14 @@ class StreamRouter:
|
|
|
778
866
|
|
|
779
867
|
def _consume_chat_chunk(
|
|
780
868
|
self,
|
|
781
|
-
payload:
|
|
782
|
-
reasoning_parts:
|
|
783
|
-
text_parts:
|
|
784
|
-
tool_calls:
|
|
785
|
-
current_usage:
|
|
786
|
-
finish_reasons:
|
|
787
|
-
) ->
|
|
788
|
-
events:
|
|
869
|
+
payload: "typing.Dict[str, object]",
|
|
870
|
+
reasoning_parts: "typing.List[str]",
|
|
871
|
+
text_parts: "typing.List[str]",
|
|
872
|
+
tool_calls: "typing.Dict[int, typing.Dict[str, object]]",
|
|
873
|
+
current_usage: "typing.Dict[str, object]",
|
|
874
|
+
finish_reasons: "typing.List[str]",
|
|
875
|
+
) -> "typing.List[typing.Tuple[str, typing.Dict[str, object]]]":
|
|
876
|
+
events: "typing.List[typing.Tuple[str, typing.Dict[str, object]]]" = []
|
|
789
877
|
usage = payload.get("usage")
|
|
790
878
|
if isinstance(usage, dict):
|
|
791
879
|
self._capture_usage_snapshot(current_usage, usage)
|
|
@@ -876,9 +964,9 @@ class StreamRouter:
|
|
|
876
964
|
|
|
877
965
|
def _capture_usage_snapshot(
|
|
878
966
|
self,
|
|
879
|
-
current_usage:
|
|
880
|
-
usage:
|
|
881
|
-
) ->
|
|
967
|
+
current_usage: "typing.Dict[str, object]",
|
|
968
|
+
usage: "typing.Dict[str, object]",
|
|
969
|
+
) -> "None":
|
|
882
970
|
scalar_mappings = (
|
|
883
971
|
("input_tokens", usage.get("input_tokens", usage.get("prompt_tokens"))),
|
|
884
972
|
(
|
|
@@ -910,12 +998,12 @@ class StreamRouter:
|
|
|
910
998
|
|
|
911
999
|
def _build_output_items(
|
|
912
1000
|
self,
|
|
913
|
-
reasoning_parts:
|
|
914
|
-
text_parts:
|
|
915
|
-
tool_calls:
|
|
916
|
-
custom_tool_names:
|
|
917
|
-
) ->
|
|
918
|
-
items:
|
|
1001
|
+
reasoning_parts: "typing.List[str]",
|
|
1002
|
+
text_parts: "typing.List[str]",
|
|
1003
|
+
tool_calls: "typing.Dict[int, typing.Dict[str, object]]",
|
|
1004
|
+
custom_tool_names: "typing.Set[str]",
|
|
1005
|
+
) -> "typing.List[typing.Dict[str, object]]":
|
|
1006
|
+
items: "typing.List[typing.Dict[str, object]]" = []
|
|
919
1007
|
reasoning_text = "".join(reasoning_parts)
|
|
920
1008
|
if reasoning_text:
|
|
921
1009
|
items.append(
|
|
@@ -968,11 +1056,18 @@ class StreamRouter:
|
|
|
968
1056
|
except CustomToolAdapterError as exc:
|
|
969
1057
|
raise OutcommingChatError(str(exc)) from exc
|
|
970
1058
|
continue
|
|
1059
|
+
try:
|
|
1060
|
+
json.loads(arguments)
|
|
1061
|
+
except json.JSONDecodeError as exc:
|
|
1062
|
+
raise OutcommingChatError(
|
|
1063
|
+
f"outcomming function call arguments for {name!r} "
|
|
1064
|
+
f"contain invalid JSON: {exc}",
|
|
1065
|
+
error_type="server_error",
|
|
1066
|
+
) from exc
|
|
971
1067
|
items.append(
|
|
972
1068
|
{
|
|
973
1069
|
"type": "function_call",
|
|
974
|
-
"call_id": str(tool_call.get("id", "")).strip()
|
|
975
|
-
or f"call_{index}",
|
|
1070
|
+
"call_id": str(tool_call.get("id", "")).strip() or f"call_{index}",
|
|
976
1071
|
"name": name,
|
|
977
1072
|
"arguments": arguments,
|
|
978
1073
|
}
|
|
@@ -980,15 +1075,27 @@ class StreamRouter:
|
|
|
980
1075
|
|
|
981
1076
|
return items
|
|
982
1077
|
|
|
983
|
-
def _request_json(
|
|
1078
|
+
def _request_json(
|
|
1079
|
+
self, request: "urllib.request.Request"
|
|
1080
|
+
) -> "typing.Dict[str, object]":
|
|
1081
|
+
with self._request_diagnostics(request) as progress:
|
|
1082
|
+
return self._read_json_response(request, progress)
|
|
1083
|
+
|
|
1084
|
+
def _read_json_response(self, request, progress):
|
|
984
1085
|
try:
|
|
985
1086
|
with urllib.request.urlopen(
|
|
986
1087
|
request,
|
|
987
1088
|
context=ssl.create_default_context(),
|
|
988
1089
|
timeout=self._config.timeout_seconds,
|
|
989
1090
|
) as response:
|
|
1091
|
+
progress["headers_at"] = time.monotonic()
|
|
1092
|
+
progress["status"] = response.status
|
|
1093
|
+
progress["phase"] = "reading_response_body"
|
|
990
1094
|
return json.loads(response.read().decode("utf-8"))
|
|
991
1095
|
except urllib.error.HTTPError as exc:
|
|
1096
|
+
progress["headers_at"] = time.monotonic()
|
|
1097
|
+
progress["status"] = exc.code
|
|
1098
|
+
progress["phase"] = "reading_error_body"
|
|
992
1099
|
body = exc.read().decode("utf-8", errors="replace")
|
|
993
1100
|
raise OutcommingChatError(
|
|
994
1101
|
f"outcomming request failed with status {exc.code}: {body[:500]}"
|
|
@@ -1002,8 +1109,12 @@ class StreamRouter:
|
|
|
1002
1109
|
raise OutcommingChatError(
|
|
1003
1110
|
f"outcomming request failed: {exc.reason}"
|
|
1004
1111
|
) from exc
|
|
1112
|
+
except (OSError, EOFError, http.client.HTTPException) as exc:
|
|
1113
|
+
raise OutcommingChatError(
|
|
1114
|
+
f"outcomming request failed: {type(exc).__name__}: {exc}"
|
|
1115
|
+
) from exc
|
|
1005
1116
|
|
|
1006
|
-
def _build_headers(self, accept:
|
|
1117
|
+
def _build_headers(self, accept: "str") -> "typing.Dict[str, str]":
|
|
1007
1118
|
headers = {
|
|
1008
1119
|
"Accept": accept,
|
|
1009
1120
|
"Content-Type": "application/json",
|
|
@@ -1014,8 +1125,8 @@ class StreamRouter:
|
|
|
1014
1125
|
return headers
|
|
1015
1126
|
|
|
1016
1127
|
def _iter_sse_events(self, response):
|
|
1017
|
-
event_name:
|
|
1018
|
-
data_lines:
|
|
1128
|
+
event_name: "typing.Union[str, None]" = None
|
|
1129
|
+
data_lines: "typing.List[str]" = []
|
|
1019
1130
|
|
|
1020
1131
|
for raw_line in response:
|
|
1021
1132
|
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|