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.
- 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 +245 -983
- 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.0.dist-info/METADATA +704 -0
- python_codex-0.3.0.dist-info/RECORD +90 -0
- responses_server/__init__.py +1 -5
- responses_server/__main__.py +0 -1
- responses_server/app.py +36 -31
- 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 +101 -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 +461 -375
- workspace_server/workspace.html +852 -228
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +137 -79
- 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.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/model.py
CHANGED
|
@@ -1,39 +1,48 @@
|
|
|
1
|
-
|
|
2
1
|
import asyncio
|
|
3
2
|
import json
|
|
4
3
|
import os
|
|
5
4
|
import re
|
|
5
|
+
import threading
|
|
6
|
+
import typing
|
|
6
7
|
import urllib.parse
|
|
8
|
+
import uuid
|
|
7
9
|
from dataclasses import dataclass, field, replace
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from typing import Callable
|
|
10
|
-
from .compat import Protocol
|
|
11
12
|
|
|
12
13
|
import requests
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
from .compat import Protocol
|
|
14
16
|
|
|
15
17
|
try:
|
|
16
18
|
import tomllib
|
|
17
19
|
except ModuleNotFoundError: # pragma: no cover - Python 3.10 path
|
|
18
20
|
import tomli as tomllib
|
|
19
21
|
|
|
22
|
+
from .events import (
|
|
23
|
+
AssistantDeltaEvent,
|
|
24
|
+
ModelEvent,
|
|
25
|
+
StreamErrorEvent,
|
|
26
|
+
TokenCountEvent,
|
|
27
|
+
ToolCalledEvent,
|
|
28
|
+
)
|
|
29
|
+
from .model_metadata import model_metadata
|
|
20
30
|
from .protocol import (
|
|
21
31
|
AssistantMessage,
|
|
22
32
|
JSONDict,
|
|
33
|
+
ModelOutputItem,
|
|
23
34
|
ModelResponse,
|
|
24
|
-
ModelStreamEvent,
|
|
25
35
|
Prompt,
|
|
26
36
|
ReasoningItem,
|
|
27
37
|
ToolCall,
|
|
28
38
|
)
|
|
29
|
-
from .model_metadata import model_metadata
|
|
30
39
|
from .utils import build_user_agent, uuid7_string
|
|
31
40
|
|
|
32
41
|
DEFAULT_CODEX_CONFIG_PATH = Path.home() / ".codex" / "config.toml"
|
|
33
42
|
DEFAULT_ORIGINATOR = "pycodex"
|
|
34
43
|
RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
|
|
35
|
-
ModelStreamEventHandler = Callable[[
|
|
36
|
-
NOOP_MODEL_STREAM_EVENT_HANDLER:
|
|
44
|
+
ModelStreamEventHandler = Callable[[ModelEvent], None]
|
|
45
|
+
NOOP_MODEL_STREAM_EVENT_HANDLER: "ModelStreamEventHandler" = lambda _event: None
|
|
37
46
|
DEFAULT_STREAM_MAX_RETRIES = 5
|
|
38
47
|
DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000
|
|
39
48
|
INITIAL_RETRY_DELAY_SECONDS = 0.2
|
|
@@ -44,39 +53,52 @@ RATE_LIMIT_RETRY_AFTER_RE = re.compile(
|
|
|
44
53
|
|
|
45
54
|
|
|
46
55
|
class ModelClient(Protocol):
|
|
56
|
+
@property
|
|
57
|
+
def model(self) -> "str":
|
|
58
|
+
"""Return the current model identifier."""
|
|
59
|
+
|
|
47
60
|
async def complete(
|
|
48
61
|
self,
|
|
49
|
-
prompt:
|
|
50
|
-
event_handler:
|
|
51
|
-
) ->
|
|
62
|
+
prompt: "Prompt",
|
|
63
|
+
event_handler: "ModelStreamEventHandler" = NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
64
|
+
) -> "ModelResponse":
|
|
52
65
|
"""Return the next batch of model output items for the current prompt."""
|
|
53
66
|
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
class ModelControl(Protocol):
|
|
69
|
+
model: "str"
|
|
70
|
+
|
|
71
|
+
async def list_models(self) -> "typing.List[str]":
|
|
72
|
+
"""List the models available to an interactive session."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(
|
|
76
|
+
frozen=True,
|
|
77
|
+
)
|
|
56
78
|
class ResponsesProviderConfig:
|
|
57
|
-
model:
|
|
58
|
-
provider_name:
|
|
59
|
-
base_url:
|
|
60
|
-
api_key_env:
|
|
61
|
-
use_chat_completion:
|
|
62
|
-
wire_api:
|
|
63
|
-
query_params:
|
|
64
|
-
reasoning_effort:
|
|
65
|
-
reasoning_summary:
|
|
66
|
-
verbosity:
|
|
67
|
-
sandbox_mode:
|
|
68
|
-
beta_features_header:
|
|
69
|
-
stream_max_retries:
|
|
70
|
-
stream_idle_timeout_ms:
|
|
71
|
-
service_tier:
|
|
72
|
-
responses_lite_override:
|
|
79
|
+
model: "str"
|
|
80
|
+
provider_name: "str"
|
|
81
|
+
base_url: "str"
|
|
82
|
+
api_key_env: "typing.Union[str, None]"
|
|
83
|
+
use_chat_completion: "bool" = False
|
|
84
|
+
wire_api: "str" = "responses"
|
|
85
|
+
query_params: "typing.Dict[str, str]" = field(default_factory=dict)
|
|
86
|
+
reasoning_effort: "typing.Union[str, None]" = None
|
|
87
|
+
reasoning_summary: "typing.Union[str, None]" = None
|
|
88
|
+
verbosity: "typing.Union[str, None]" = None
|
|
89
|
+
sandbox_mode: "typing.Union[str, None]" = None
|
|
90
|
+
beta_features_header: "typing.Union[str, None]" = None
|
|
91
|
+
stream_max_retries: "typing.Union[int, None]" = None
|
|
92
|
+
stream_idle_timeout_ms: "typing.Union[int, None]" = None
|
|
93
|
+
service_tier: "typing.Union[str, None]" = None
|
|
94
|
+
responses_lite_override: "typing.Union[bool, None]" = None
|
|
73
95
|
|
|
74
96
|
@classmethod
|
|
75
97
|
def from_codex_config(
|
|
76
98
|
cls,
|
|
77
|
-
config_path:
|
|
78
|
-
profile:
|
|
79
|
-
) ->
|
|
99
|
+
config_path: "typing.Union[str, Path]" = DEFAULT_CODEX_CONFIG_PATH,
|
|
100
|
+
profile: "typing.Union[str, None]" = None,
|
|
101
|
+
) -> "ResponsesProviderConfig":
|
|
80
102
|
data = tomllib.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
81
103
|
selected = dict(data)
|
|
82
104
|
if profile is not None:
|
|
@@ -98,16 +120,12 @@ class ResponsesProviderConfig:
|
|
|
98
120
|
for key, value in provider.get("query_params", {}).items()
|
|
99
121
|
}
|
|
100
122
|
features = selected.get("features", {})
|
|
101
|
-
beta_features:
|
|
123
|
+
beta_features: "typing.List[str]" = []
|
|
102
124
|
if isinstance(features, dict) and features.get("guardian_approval") is True:
|
|
103
125
|
beta_features.append("guardian_approval")
|
|
104
|
-
use_chat_completion = _optional_bool(
|
|
105
|
-
selected.get("use_chat_completion")
|
|
106
|
-
)
|
|
126
|
+
use_chat_completion = _optional_bool(selected.get("use_chat_completion"))
|
|
107
127
|
if use_chat_completion is None:
|
|
108
|
-
use_chat_completion = _optional_bool(
|
|
109
|
-
provider.get("use_chat_completion")
|
|
110
|
-
)
|
|
128
|
+
use_chat_completion = _optional_bool(provider.get("use_chat_completion"))
|
|
111
129
|
if use_chat_completion is None:
|
|
112
130
|
use_chat_completion = False
|
|
113
131
|
return cls(
|
|
@@ -125,10 +143,12 @@ class ResponsesProviderConfig:
|
|
|
125
143
|
sandbox_mode=selected.get("sandbox_mode"),
|
|
126
144
|
beta_features_header=",".join(beta_features) or None,
|
|
127
145
|
stream_max_retries=_optional_int(provider.get("stream_max_retries")),
|
|
128
|
-
stream_idle_timeout_ms=_optional_int(
|
|
146
|
+
stream_idle_timeout_ms=_optional_int(
|
|
147
|
+
provider.get("stream_idle_timeout_ms")
|
|
148
|
+
),
|
|
129
149
|
)
|
|
130
150
|
|
|
131
|
-
def api_key(self) ->
|
|
151
|
+
def api_key(self) -> "typing.Union[str, None]":
|
|
132
152
|
if not self.api_key_env:
|
|
133
153
|
return None
|
|
134
154
|
value = os.environ.get(self.api_key_env, "")
|
|
@@ -140,33 +160,31 @@ class ResponsesProviderConfig:
|
|
|
140
160
|
|
|
141
161
|
def with_overrides(
|
|
142
162
|
self,
|
|
143
|
-
model:
|
|
144
|
-
reasoning_effort:
|
|
145
|
-
) ->
|
|
163
|
+
model: "typing.Union[str, None]" = None,
|
|
164
|
+
reasoning_effort: "typing.Union[str, None]" = None,
|
|
165
|
+
) -> "ResponsesProviderConfig":
|
|
146
166
|
return replace(
|
|
147
167
|
self,
|
|
148
168
|
model=self.model if model is None else model,
|
|
149
169
|
reasoning_effort=(
|
|
150
|
-
self.reasoning_effort
|
|
151
|
-
if reasoning_effort is None
|
|
152
|
-
else reasoning_effort
|
|
170
|
+
self.reasoning_effort if reasoning_effort is None else reasoning_effort
|
|
153
171
|
),
|
|
154
172
|
)
|
|
155
173
|
|
|
156
|
-
def effective_stream_max_retries(self) ->
|
|
174
|
+
def effective_stream_max_retries(self) -> "int":
|
|
157
175
|
if self.stream_max_retries is None:
|
|
158
176
|
return DEFAULT_STREAM_MAX_RETRIES
|
|
159
177
|
return max(int(self.stream_max_retries), 0)
|
|
160
178
|
|
|
161
|
-
def effective_stream_idle_timeout_seconds(self) ->
|
|
179
|
+
def effective_stream_idle_timeout_seconds(self) -> "float":
|
|
162
180
|
if self.stream_idle_timeout_ms is None:
|
|
163
181
|
return DEFAULT_STREAM_IDLE_TIMEOUT_MS / 1000.0
|
|
164
182
|
return max(int(self.stream_idle_timeout_ms), 1) / 1000.0
|
|
165
183
|
|
|
166
|
-
def metadata(self) ->
|
|
184
|
+
def metadata(self) -> "typing.Union[JSONDict, None]":
|
|
167
185
|
return model_metadata(self.model)
|
|
168
186
|
|
|
169
|
-
def use_responses_lite(self) ->
|
|
187
|
+
def use_responses_lite(self) -> "bool":
|
|
170
188
|
if self.responses_lite_override is not None:
|
|
171
189
|
return self.responses_lite_override
|
|
172
190
|
metadata = self.metadata()
|
|
@@ -174,7 +192,7 @@ class ResponsesProviderConfig:
|
|
|
174
192
|
return False
|
|
175
193
|
return metadata.get("use_responses_lite") is True
|
|
176
194
|
|
|
177
|
-
def effective_reasoning_effort(self) ->
|
|
195
|
+
def effective_reasoning_effort(self) -> "typing.Union[str, None]":
|
|
178
196
|
if self.reasoning_effort is not None:
|
|
179
197
|
return str(self.reasoning_effort)
|
|
180
198
|
metadata = self.metadata()
|
|
@@ -182,7 +200,7 @@ class ResponsesProviderConfig:
|
|
|
182
200
|
return None
|
|
183
201
|
return _optional_metadata_string(metadata, "default_reasoning_level")
|
|
184
202
|
|
|
185
|
-
def effective_reasoning_summary(self) ->
|
|
203
|
+
def effective_reasoning_summary(self) -> "typing.Union[str, None]":
|
|
186
204
|
summary = self.reasoning_summary
|
|
187
205
|
if summary is None:
|
|
188
206
|
metadata = self.metadata()
|
|
@@ -195,7 +213,7 @@ class ResponsesProviderConfig:
|
|
|
195
213
|
return None
|
|
196
214
|
return str(summary)
|
|
197
215
|
|
|
198
|
-
def effective_verbosity(self) ->
|
|
216
|
+
def effective_verbosity(self) -> "typing.Union[str, None]":
|
|
199
217
|
if self.verbosity is not None:
|
|
200
218
|
return str(self.verbosity)
|
|
201
219
|
metadata = self.metadata()
|
|
@@ -203,7 +221,7 @@ class ResponsesProviderConfig:
|
|
|
203
221
|
return None
|
|
204
222
|
return _optional_metadata_string(metadata, "default_verbosity")
|
|
205
223
|
|
|
206
|
-
def effective_service_tier(self) ->
|
|
224
|
+
def effective_service_tier(self) -> "typing.Union[str, None]":
|
|
207
225
|
service_tier = self.service_tier
|
|
208
226
|
if service_tier is None:
|
|
209
227
|
return None
|
|
@@ -225,7 +243,9 @@ class ResponsesProviderConfig:
|
|
|
225
243
|
return None
|
|
226
244
|
|
|
227
245
|
|
|
228
|
-
def _optional_bool(
|
|
246
|
+
def _optional_bool(
|
|
247
|
+
value: "typing.Union[bool, str, int, None]",
|
|
248
|
+
) -> "typing.Union[bool, None]":
|
|
229
249
|
if value is None:
|
|
230
250
|
return None
|
|
231
251
|
if isinstance(value, bool):
|
|
@@ -239,15 +259,15 @@ def _optional_bool(value: 'typing.Union[bool, str, int, None]') -> 'typing.Union
|
|
|
239
259
|
|
|
240
260
|
|
|
241
261
|
def _metadata_supports_reasoning(
|
|
242
|
-
metadata:
|
|
243
|
-
) ->
|
|
262
|
+
metadata: "typing.Union[JSONDict, None]",
|
|
263
|
+
) -> "bool":
|
|
244
264
|
return metadata is not None and metadata.get("supports_reasoning_summaries") is True
|
|
245
265
|
|
|
246
266
|
|
|
247
267
|
def _optional_metadata_string(
|
|
248
|
-
metadata:
|
|
249
|
-
key:
|
|
250
|
-
) ->
|
|
268
|
+
metadata: "typing.Union[JSONDict, None]",
|
|
269
|
+
key: "str",
|
|
270
|
+
) -> "typing.Union[str, None]":
|
|
251
271
|
if metadata is None:
|
|
252
272
|
return None
|
|
253
273
|
value = metadata.get(key)
|
|
@@ -257,7 +277,7 @@ def _optional_metadata_string(
|
|
|
257
277
|
return text or None
|
|
258
278
|
|
|
259
279
|
|
|
260
|
-
def _strip_image_details(items:
|
|
280
|
+
def _strip_image_details(items: "typing.Iterable[object]") -> "None":
|
|
261
281
|
for item in items:
|
|
262
282
|
if not isinstance(item, dict):
|
|
263
283
|
continue
|
|
@@ -269,7 +289,7 @@ def _strip_image_details(items: 'typing.Iterable[object]') -> 'None':
|
|
|
269
289
|
_strip_image_detail_from_content_items(output)
|
|
270
290
|
|
|
271
291
|
|
|
272
|
-
def _strip_image_detail_from_content_items(items:
|
|
292
|
+
def _strip_image_detail_from_content_items(items: "typing.Iterable[object]") -> "None":
|
|
273
293
|
for item in items:
|
|
274
294
|
if isinstance(item, dict) and item.get("type") == "input_image":
|
|
275
295
|
item.pop("detail", None)
|
|
@@ -279,13 +299,39 @@ class ResponsesApiError(RuntimeError):
|
|
|
279
299
|
pass
|
|
280
300
|
|
|
281
301
|
|
|
302
|
+
class ContextLengthExceeded(ResponsesApiError):
|
|
303
|
+
def __init__(self, message: "str") -> "None":
|
|
304
|
+
super().__init__(message)
|
|
305
|
+
self.usage: "typing.Union[typing.Dict[str, int], None]" = None
|
|
306
|
+
self.token_limit: "typing.Union[int, None]" = None
|
|
307
|
+
requested = re.search(r"requested\s+([0-9,]+)\s+tokens", message, re.IGNORECASE)
|
|
308
|
+
if requested is not None:
|
|
309
|
+
total = int(requested.group(1).replace(",", ""))
|
|
310
|
+
self.usage = {"total_tokens": total, "input_tokens": total}
|
|
311
|
+
split = re.search(
|
|
312
|
+
r"\(([0-9,]+)\s+in\s+the\s+messages,\s+([0-9,]+)\s+in\s+the\s+completion\)",
|
|
313
|
+
message,
|
|
314
|
+
re.IGNORECASE,
|
|
315
|
+
)
|
|
316
|
+
if split is not None:
|
|
317
|
+
self.usage["input_tokens"] = int(split.group(1).replace(",", ""))
|
|
318
|
+
self.usage["output_tokens"] = int(split.group(2).replace(",", ""))
|
|
319
|
+
limit = re.search(
|
|
320
|
+
r"maximum\s+context\s+length\s+is\s+([0-9,]+)\s+tokens",
|
|
321
|
+
message,
|
|
322
|
+
re.IGNORECASE,
|
|
323
|
+
)
|
|
324
|
+
if limit is not None:
|
|
325
|
+
self.token_limit = int(limit.group(1).replace(",", ""))
|
|
326
|
+
|
|
327
|
+
|
|
282
328
|
class ResponsesIncompleteError(ResponsesApiError):
|
|
283
329
|
def __init__(
|
|
284
330
|
self,
|
|
285
|
-
message:
|
|
286
|
-
partial_items:
|
|
287
|
-
reason:
|
|
288
|
-
) ->
|
|
331
|
+
message: "str",
|
|
332
|
+
partial_items: "typing.Sequence[ModelOutputItem]",
|
|
333
|
+
reason: "str" = "",
|
|
334
|
+
) -> "None":
|
|
289
335
|
super().__init__(message)
|
|
290
336
|
self.partial_items = tuple(partial_items)
|
|
291
337
|
self.reason = reason
|
|
@@ -294,21 +340,21 @@ class ResponsesIncompleteError(ResponsesApiError):
|
|
|
294
340
|
class ResponsesRetryableError(ResponsesApiError):
|
|
295
341
|
def __init__(
|
|
296
342
|
self,
|
|
297
|
-
message:
|
|
298
|
-
retry_delay_seconds:
|
|
299
|
-
) ->
|
|
343
|
+
message: "str",
|
|
344
|
+
retry_delay_seconds: "typing.Union[float, None]" = None,
|
|
345
|
+
) -> "None":
|
|
300
346
|
super().__init__(message)
|
|
301
347
|
self.retry_delay_seconds = retry_delay_seconds
|
|
302
348
|
|
|
303
349
|
|
|
304
350
|
@dataclass
|
|
305
351
|
class _StreamDiagnostics:
|
|
306
|
-
raw_lines_received:
|
|
307
|
-
sse_events_received:
|
|
308
|
-
output_items_received:
|
|
309
|
-
last_sse_event_name:
|
|
310
|
-
last_event_type:
|
|
311
|
-
last_payload_excerpt:
|
|
352
|
+
raw_lines_received: "int" = 0
|
|
353
|
+
sse_events_received: "int" = 0
|
|
354
|
+
output_items_received: "int" = 0
|
|
355
|
+
last_sse_event_name: "str" = ""
|
|
356
|
+
last_event_type: "str" = ""
|
|
357
|
+
last_payload_excerpt: "str" = ""
|
|
312
358
|
|
|
313
359
|
|
|
314
360
|
class ResponsesModelClient:
|
|
@@ -321,40 +367,49 @@ class ResponsesModelClient:
|
|
|
321
367
|
|
|
322
368
|
def __init__(
|
|
323
369
|
self,
|
|
324
|
-
config:
|
|
325
|
-
timeout_seconds:
|
|
326
|
-
session_id:
|
|
327
|
-
originator:
|
|
328
|
-
user_agent:
|
|
329
|
-
openai_subagent:
|
|
330
|
-
) ->
|
|
370
|
+
config: "ResponsesProviderConfig",
|
|
371
|
+
timeout_seconds: "float" = 120.0,
|
|
372
|
+
session_id: "typing.Union[str, None]" = None,
|
|
373
|
+
originator: "str" = DEFAULT_ORIGINATOR,
|
|
374
|
+
user_agent: "typing.Union[str, None]" = None,
|
|
375
|
+
openai_subagent: "typing.Union[str, None]" = None,
|
|
376
|
+
) -> "None":
|
|
331
377
|
self._config = config
|
|
332
|
-
self.model = config.model
|
|
333
378
|
self._timeout_seconds = timeout_seconds
|
|
334
379
|
self._session_id = session_id or uuid7_string()
|
|
335
380
|
self._originator = originator
|
|
336
381
|
self._user_agent = user_agent or build_user_agent(originator)
|
|
337
382
|
self._openai_subagent = openai_subagent
|
|
338
383
|
|
|
384
|
+
@property
|
|
385
|
+
def model(self) -> "str":
|
|
386
|
+
return self._config.model
|
|
387
|
+
|
|
388
|
+
@model.setter
|
|
389
|
+
def model(self, model: "str") -> "None":
|
|
390
|
+
self._config = replace(self._config, model=model)
|
|
391
|
+
|
|
339
392
|
@classmethod
|
|
340
393
|
def from_codex_config(
|
|
341
394
|
cls,
|
|
342
|
-
config_path:
|
|
343
|
-
profile:
|
|
344
|
-
timeout_seconds:
|
|
345
|
-
originator:
|
|
346
|
-
user_agent:
|
|
347
|
-
) ->
|
|
395
|
+
config_path: "typing.Union[str, Path]" = DEFAULT_CODEX_CONFIG_PATH,
|
|
396
|
+
profile: "typing.Union[str, None]" = None,
|
|
397
|
+
timeout_seconds: "float" = 120.0,
|
|
398
|
+
originator: "str" = DEFAULT_ORIGINATOR,
|
|
399
|
+
user_agent: "typing.Union[str, None]" = None,
|
|
400
|
+
) -> "ResponsesModelClient":
|
|
348
401
|
config = ResponsesProviderConfig.from_codex_config(config_path, profile)
|
|
349
|
-
return cls(
|
|
402
|
+
return cls(
|
|
403
|
+
config, timeout_seconds, originator=originator, user_agent=user_agent
|
|
404
|
+
)
|
|
350
405
|
|
|
351
406
|
def with_overrides(
|
|
352
407
|
self,
|
|
353
|
-
model:
|
|
354
|
-
reasoning_effort:
|
|
355
|
-
session_id:
|
|
356
|
-
openai_subagent:
|
|
357
|
-
) ->
|
|
408
|
+
model: "typing.Union[str, None]" = None,
|
|
409
|
+
reasoning_effort: "typing.Union[str, None]" = None,
|
|
410
|
+
session_id: "typing.Union[str, None]" = None,
|
|
411
|
+
openai_subagent: "typing.Union[str, None]" = None,
|
|
412
|
+
) -> "ResponsesModelClient":
|
|
358
413
|
return ResponsesModelClient(
|
|
359
414
|
self._config.with_overrides(
|
|
360
415
|
model or self.model,
|
|
@@ -365,82 +420,85 @@ class ResponsesModelClient:
|
|
|
365
420
|
originator=self._originator,
|
|
366
421
|
user_agent=self._user_agent,
|
|
367
422
|
openai_subagent=(
|
|
368
|
-
self._openai_subagent
|
|
369
|
-
if openai_subagent is None
|
|
370
|
-
else openai_subagent
|
|
423
|
+
self._openai_subagent if openai_subagent is None else openai_subagent
|
|
371
424
|
),
|
|
372
425
|
)
|
|
373
426
|
|
|
374
|
-
def responses_url(self) ->
|
|
427
|
+
def responses_url(self) -> "str":
|
|
375
428
|
base_url = self._config.base_url.rstrip("/")
|
|
376
429
|
url = f"{base_url}/responses"
|
|
377
430
|
if self._config.query_params:
|
|
378
431
|
return f"{url}?{urllib.parse.urlencode(self._config.query_params)}"
|
|
379
432
|
return url
|
|
380
433
|
|
|
381
|
-
def models_url(self) ->
|
|
434
|
+
def models_url(self) -> "str":
|
|
382
435
|
base_url = self._config.base_url.rstrip("/")
|
|
383
436
|
url = f"{base_url}/models"
|
|
384
437
|
if self._config.query_params:
|
|
385
438
|
return f"{url}?{urllib.parse.urlencode(self._config.query_params)}"
|
|
386
439
|
return url
|
|
387
440
|
|
|
388
|
-
async def list_models(self) ->
|
|
441
|
+
async def list_models(self) -> "typing.List[str]":
|
|
389
442
|
return await asyncio.to_thread(self.list_models_sync)
|
|
390
443
|
|
|
391
|
-
def list_models_sync(self) ->
|
|
444
|
+
def list_models_sync(self) -> "typing.List[str]":
|
|
392
445
|
return self._list_models_sync()
|
|
393
446
|
|
|
394
447
|
async def complete(
|
|
395
448
|
self,
|
|
396
|
-
prompt:
|
|
397
|
-
event_handler:
|
|
398
|
-
) ->
|
|
449
|
+
prompt: "Prompt",
|
|
450
|
+
event_handler: "ModelStreamEventHandler" = NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
451
|
+
) -> "ModelResponse":
|
|
399
452
|
retries = 0
|
|
400
453
|
max_retries = self._config.effective_stream_max_retries()
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
)
|
|
422
|
-
if delay_seconds is None:
|
|
423
|
-
delay_seconds = self._retry_delay_seconds(retries)
|
|
424
|
-
event_handler(
|
|
425
|
-
ModelStreamEvent(
|
|
426
|
-
kind="stream_error",
|
|
427
|
-
payload={
|
|
428
|
-
"message": f"Reconnecting... {retries}/{max_retries}",
|
|
429
|
-
"attempt": retries,
|
|
430
|
-
"max_retries": max_retries,
|
|
431
|
-
"delay_seconds": delay_seconds,
|
|
432
|
-
"error": str(exc),
|
|
433
|
-
},
|
|
454
|
+
loop = asyncio.get_running_loop()
|
|
455
|
+
active = True
|
|
456
|
+
callback_lock = threading.Lock()
|
|
457
|
+
|
|
458
|
+
def deliver(event):
|
|
459
|
+
if active:
|
|
460
|
+
event_handler(event)
|
|
461
|
+
|
|
462
|
+
def receive(event):
|
|
463
|
+
with callback_lock:
|
|
464
|
+
if active:
|
|
465
|
+
loop.call_soon_threadsafe(deliver, event)
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
while True:
|
|
469
|
+
try:
|
|
470
|
+
return await asyncio.to_thread(
|
|
471
|
+
self._complete_sync,
|
|
472
|
+
prompt,
|
|
473
|
+
receive,
|
|
434
474
|
)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
475
|
+
except ResponsesRetryableError as exc:
|
|
476
|
+
if retries >= max_retries:
|
|
477
|
+
raise
|
|
478
|
+
retries += 1
|
|
479
|
+
delay_seconds = exc.retry_delay_seconds
|
|
480
|
+
if delay_seconds is None:
|
|
481
|
+
delay_seconds = self._retry_delay_seconds(retries)
|
|
482
|
+
event_handler(
|
|
483
|
+
StreamErrorEvent(
|
|
484
|
+
f"Reconnecting... {retries}/{max_retries}",
|
|
485
|
+
retries,
|
|
486
|
+
max_retries,
|
|
487
|
+
delay_seconds,
|
|
488
|
+
str(exc),
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
if delay_seconds > 0:
|
|
492
|
+
await asyncio.sleep(delay_seconds)
|
|
493
|
+
finally:
|
|
494
|
+
with callback_lock:
|
|
495
|
+
active = False
|
|
438
496
|
|
|
439
497
|
def _complete_sync(
|
|
440
498
|
self,
|
|
441
|
-
prompt:
|
|
442
|
-
event_handler:
|
|
443
|
-
) ->
|
|
499
|
+
prompt: "Prompt",
|
|
500
|
+
event_handler: "ModelStreamEventHandler",
|
|
501
|
+
) -> "ModelResponse":
|
|
444
502
|
payload = self._build_payload(prompt)
|
|
445
503
|
body = json.dumps(payload).encode("utf-8")
|
|
446
504
|
url = self.responses_url()
|
|
@@ -481,6 +539,8 @@ class ResponsesModelClient:
|
|
|
481
539
|
f"responses request failed with status {response.status_code}: "
|
|
482
540
|
f"{error_body[:500]}"
|
|
483
541
|
)
|
|
542
|
+
if _is_context_length_error_message(error_body):
|
|
543
|
+
raise ContextLengthExceeded(message)
|
|
484
544
|
if response.status_code >= 500:
|
|
485
545
|
raise ResponsesRetryableError(message)
|
|
486
546
|
raise ResponsesApiError(message)
|
|
@@ -498,26 +558,37 @@ class ResponsesModelClient:
|
|
|
498
558
|
self._format_transport_error(url, exc, diagnostics)
|
|
499
559
|
) from exc
|
|
500
560
|
|
|
501
|
-
def _build_payload(self, prompt:
|
|
561
|
+
def _build_payload(self, prompt: "Prompt") -> "typing.Dict[str, object]":
|
|
502
562
|
use_responses_lite = self._config.use_responses_lite()
|
|
503
563
|
input_items = [item.serialize() for item in prompt.input]
|
|
504
564
|
if use_responses_lite:
|
|
505
565
|
_strip_image_details(input_items)
|
|
506
566
|
|
|
507
567
|
tools = [tool.serialize() for tool in prompt.tools]
|
|
508
|
-
payload:
|
|
568
|
+
payload: "typing.Dict[str, object]" = {
|
|
509
569
|
"model": self.model,
|
|
510
570
|
"input": input_items,
|
|
511
|
-
"parallel_tool_calls": prompt.parallel_tool_calls
|
|
571
|
+
"parallel_tool_calls": prompt.parallel_tool_calls
|
|
572
|
+
and not use_responses_lite,
|
|
512
573
|
"store": False,
|
|
513
574
|
"stream": True,
|
|
514
575
|
"include": ["reasoning.encrypted_content"],
|
|
515
576
|
"prompt_cache_key": self._session_id,
|
|
516
577
|
}
|
|
517
578
|
if use_responses_lite:
|
|
518
|
-
|
|
579
|
+
prefix_namespace = uuid.uuid5(uuid.NAMESPACE_OID, self._session_id)
|
|
580
|
+
prefix: "typing.List[typing.Dict[str, object]]" = [
|
|
519
581
|
{
|
|
520
582
|
"type": "additional_tools",
|
|
583
|
+
"id": "at_"
|
|
584
|
+
+ str(
|
|
585
|
+
uuid.uuid5(
|
|
586
|
+
prefix_namespace,
|
|
587
|
+
json.dumps(
|
|
588
|
+
tools, ensure_ascii=False, separators=(",", ":")
|
|
589
|
+
),
|
|
590
|
+
)
|
|
591
|
+
),
|
|
521
592
|
"role": "developer",
|
|
522
593
|
"tools": tools,
|
|
523
594
|
}
|
|
@@ -526,6 +597,8 @@ class ResponsesModelClient:
|
|
|
526
597
|
prefix.append(
|
|
527
598
|
{
|
|
528
599
|
"type": "message",
|
|
600
|
+
"id": "msg_"
|
|
601
|
+
+ str(uuid.uuid5(prefix_namespace, prompt.base_instructions)),
|
|
529
602
|
"role": "developer",
|
|
530
603
|
"content": [
|
|
531
604
|
{
|
|
@@ -543,7 +616,7 @@ class ResponsesModelClient:
|
|
|
543
616
|
if prompt.tools or use_responses_lite:
|
|
544
617
|
payload["tool_choice"] = "auto"
|
|
545
618
|
|
|
546
|
-
reasoning:
|
|
619
|
+
reasoning: "typing.Dict[str, str]" = {}
|
|
547
620
|
reasoning_effort = self._config.effective_reasoning_effort()
|
|
548
621
|
reasoning_summary = self._config.effective_reasoning_summary()
|
|
549
622
|
if reasoning_effort is not None:
|
|
@@ -568,7 +641,7 @@ class ResponsesModelClient:
|
|
|
568
641
|
|
|
569
642
|
return payload
|
|
570
643
|
|
|
571
|
-
def _list_models_sync(self) ->
|
|
644
|
+
def _list_models_sync(self) -> "typing.List[str]":
|
|
572
645
|
prepared = requests.PreparedRequest()
|
|
573
646
|
prepared.prepare(
|
|
574
647
|
method="GET",
|
|
@@ -606,7 +679,7 @@ class ResponsesModelClient:
|
|
|
606
679
|
data = payload.get("data")
|
|
607
680
|
if not isinstance(data, list):
|
|
608
681
|
raise ResponsesApiError("models response is missing `data` list")
|
|
609
|
-
models:
|
|
682
|
+
models: "typing.List[str]" = []
|
|
610
683
|
for item in data:
|
|
611
684
|
if not isinstance(item, dict):
|
|
612
685
|
continue
|
|
@@ -615,7 +688,7 @@ class ResponsesModelClient:
|
|
|
615
688
|
models.append(model_id)
|
|
616
689
|
return models
|
|
617
690
|
|
|
618
|
-
def _build_headers(self, prompt:
|
|
691
|
+
def _build_headers(self, prompt: "Prompt") -> "typing.Dict[str, str]":
|
|
619
692
|
headers = {
|
|
620
693
|
"content-type": "application/json",
|
|
621
694
|
"accept": "text/event-stream",
|
|
@@ -640,7 +713,7 @@ class ResponsesModelClient:
|
|
|
640
713
|
)
|
|
641
714
|
return headers
|
|
642
715
|
|
|
643
|
-
def _build_model_list_headers(self) ->
|
|
716
|
+
def _build_model_list_headers(self) -> "typing.Dict[str, str]":
|
|
644
717
|
headers = {
|
|
645
718
|
"accept": "application/json",
|
|
646
719
|
"originator": self._originator,
|
|
@@ -658,10 +731,10 @@ class ResponsesModelClient:
|
|
|
658
731
|
def _parse_stream(
|
|
659
732
|
self,
|
|
660
733
|
response,
|
|
661
|
-
event_handler:
|
|
662
|
-
diagnostics:
|
|
663
|
-
) ->
|
|
664
|
-
items:
|
|
734
|
+
event_handler: "ModelStreamEventHandler",
|
|
735
|
+
diagnostics: "typing.Union[_StreamDiagnostics, None]" = None,
|
|
736
|
+
) -> "ModelResponse":
|
|
737
|
+
items: "typing.List[typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem]]" = ([])
|
|
665
738
|
saw_completed = False
|
|
666
739
|
last_event_type = ""
|
|
667
740
|
|
|
@@ -680,12 +753,7 @@ class ResponsesModelClient:
|
|
|
680
753
|
diagnostics.last_event_type = last_event_type
|
|
681
754
|
|
|
682
755
|
if event_type == "response.output_text.delta":
|
|
683
|
-
event_handler(
|
|
684
|
-
ModelStreamEvent(
|
|
685
|
-
kind="assistant_delta",
|
|
686
|
-
payload={"delta": str(payload.get("delta", ""))},
|
|
687
|
-
)
|
|
688
|
-
)
|
|
756
|
+
event_handler(AssistantDeltaEvent(str(payload.get("delta", ""))))
|
|
689
757
|
continue
|
|
690
758
|
|
|
691
759
|
if event_type == "response.output_item.done":
|
|
@@ -695,31 +763,41 @@ class ResponsesModelClient:
|
|
|
695
763
|
and item_payload.get("type") == "web_search_call"
|
|
696
764
|
):
|
|
697
765
|
action_payload = item_payload.get("action")
|
|
698
|
-
|
|
699
|
-
"call_id": str(item_payload.get("id", "web_search")),
|
|
700
|
-
"tool_name": "web_search",
|
|
701
|
-
}
|
|
766
|
+
action_type = None
|
|
702
767
|
if isinstance(action_payload, dict):
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
event_payload["query"] = str(action_payload.get("query", ""))
|
|
708
|
-
queries = action_payload.get("queries")
|
|
709
|
-
if isinstance(queries, list):
|
|
710
|
-
event_payload["queries"] = [
|
|
711
|
-
str(query) for query in queries if str(query).strip()
|
|
712
|
-
]
|
|
713
|
-
if "url" in action_payload:
|
|
714
|
-
event_payload["url"] = str(action_payload.get("url", ""))
|
|
715
|
-
if "pattern" in action_payload:
|
|
716
|
-
event_payload["pattern"] = str(
|
|
717
|
-
action_payload.get("pattern", "")
|
|
718
|
-
)
|
|
768
|
+
action_type = str(action_payload.get("type", ""))
|
|
769
|
+
else:
|
|
770
|
+
action_payload = {}
|
|
771
|
+
queries = action_payload.get("queries")
|
|
719
772
|
event_handler(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
773
|
+
ToolCalledEvent(
|
|
774
|
+
call_id=str(item_payload.get("id", "web_search")),
|
|
775
|
+
tool_name="web_search",
|
|
776
|
+
action_type=action_type,
|
|
777
|
+
query=(
|
|
778
|
+
str(action_payload["query"])
|
|
779
|
+
if "query" in action_payload
|
|
780
|
+
else None
|
|
781
|
+
),
|
|
782
|
+
queries=(
|
|
783
|
+
tuple(
|
|
784
|
+
str(query)
|
|
785
|
+
for query in queries
|
|
786
|
+
if str(query).strip()
|
|
787
|
+
)
|
|
788
|
+
if isinstance(queries, list)
|
|
789
|
+
else None
|
|
790
|
+
),
|
|
791
|
+
url=(
|
|
792
|
+
str(action_payload["url"])
|
|
793
|
+
if "url" in action_payload
|
|
794
|
+
else None
|
|
795
|
+
),
|
|
796
|
+
pattern=(
|
|
797
|
+
str(action_payload["pattern"])
|
|
798
|
+
if "pattern" in action_payload
|
|
799
|
+
else None
|
|
800
|
+
),
|
|
723
801
|
)
|
|
724
802
|
)
|
|
725
803
|
continue
|
|
@@ -727,15 +805,7 @@ class ResponsesModelClient:
|
|
|
727
805
|
parsed = self._parse_output_item(item_payload)
|
|
728
806
|
if parsed is not None:
|
|
729
807
|
if isinstance(parsed, ToolCall):
|
|
730
|
-
event_handler(
|
|
731
|
-
ModelStreamEvent(
|
|
732
|
-
kind="tool_call",
|
|
733
|
-
payload={
|
|
734
|
-
"call_id": parsed.call_id,
|
|
735
|
-
"tool_name": parsed.name,
|
|
736
|
-
},
|
|
737
|
-
)
|
|
738
|
-
)
|
|
808
|
+
event_handler(ToolCalledEvent(parsed.call_id, parsed.name))
|
|
739
809
|
items.append(parsed)
|
|
740
810
|
if diagnostics is not None:
|
|
741
811
|
diagnostics.output_items_received += 1
|
|
@@ -750,12 +820,7 @@ class ResponsesModelClient:
|
|
|
750
820
|
usage = dict(response_usage)
|
|
751
821
|
elif isinstance(payload.get("usage"), dict):
|
|
752
822
|
usage = dict(payload["usage"])
|
|
753
|
-
event_handler(
|
|
754
|
-
ModelStreamEvent(
|
|
755
|
-
kind="token_count",
|
|
756
|
-
payload={"usage": usage},
|
|
757
|
-
)
|
|
758
|
-
)
|
|
823
|
+
event_handler(TokenCountEvent(usage))
|
|
759
824
|
saw_completed = True
|
|
760
825
|
break
|
|
761
826
|
|
|
@@ -784,22 +849,17 @@ class ResponsesModelClient:
|
|
|
784
849
|
|
|
785
850
|
def _parse_output_item(
|
|
786
851
|
self,
|
|
787
|
-
item:
|
|
788
|
-
) ->
|
|
852
|
+
item: "typing.Dict[str, object]",
|
|
853
|
+
) -> "typing.Union[typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], None]":
|
|
789
854
|
item_type = item.get("type")
|
|
790
855
|
if item_type == "reasoning":
|
|
791
856
|
return ReasoningItem(payload=dict(item))
|
|
792
857
|
|
|
793
858
|
if item_type == "message" and item.get("role") == "assistant":
|
|
794
|
-
|
|
795
|
-
text_parts = []
|
|
796
|
-
for part in content:
|
|
797
|
-
if isinstance(part, dict) and part.get("type") == "output_text":
|
|
798
|
-
text_parts.append(str(part.get("text", "")))
|
|
799
|
-
return AssistantMessage(text="".join(text_parts))
|
|
859
|
+
return AssistantMessage.from_response_item(item)
|
|
800
860
|
|
|
801
861
|
if item_type == "function_call":
|
|
802
|
-
raw_arguments =
|
|
862
|
+
raw_arguments = item["arguments"]
|
|
803
863
|
arguments = json.loads(raw_arguments)
|
|
804
864
|
if not isinstance(arguments, dict):
|
|
805
865
|
raise ResponsesApiError(
|
|
@@ -809,6 +869,9 @@ class ResponsesModelClient:
|
|
|
809
869
|
call_id=str(item["call_id"]),
|
|
810
870
|
name=str(item["name"]),
|
|
811
871
|
arguments=arguments,
|
|
872
|
+
id=item.get("id"),
|
|
873
|
+
raw_arguments=raw_arguments,
|
|
874
|
+
namespace=item.get("namespace"),
|
|
812
875
|
)
|
|
813
876
|
|
|
814
877
|
if item_type == "custom_tool_call":
|
|
@@ -817,6 +880,9 @@ class ResponsesModelClient:
|
|
|
817
880
|
name=str(item["name"]),
|
|
818
881
|
arguments=str(item.get("input", "")),
|
|
819
882
|
tool_type="custom",
|
|
883
|
+
id=item.get("id"),
|
|
884
|
+
namespace=item.get("namespace"),
|
|
885
|
+
status=item.get("status"),
|
|
820
886
|
)
|
|
821
887
|
|
|
822
888
|
return None
|
|
@@ -824,10 +890,10 @@ class ResponsesModelClient:
|
|
|
824
890
|
def _iter_sse_events(
|
|
825
891
|
self,
|
|
826
892
|
response,
|
|
827
|
-
diagnostics:
|
|
893
|
+
diagnostics: "typing.Union[_StreamDiagnostics, None]" = None,
|
|
828
894
|
):
|
|
829
|
-
event_name:
|
|
830
|
-
data_lines:
|
|
895
|
+
event_name: "typing.Union[str, None]" = None
|
|
896
|
+
data_lines: "typing.List[str]" = []
|
|
831
897
|
|
|
832
898
|
for raw_line in response:
|
|
833
899
|
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|
|
@@ -870,7 +936,7 @@ class ResponsesModelClient:
|
|
|
870
936
|
def _track_stream_lines(
|
|
871
937
|
self,
|
|
872
938
|
response,
|
|
873
|
-
diagnostics:
|
|
939
|
+
diagnostics: "_StreamDiagnostics",
|
|
874
940
|
):
|
|
875
941
|
for raw_line in response:
|
|
876
942
|
diagnostics.raw_lines_received += 1
|
|
@@ -878,8 +944,8 @@ class ResponsesModelClient:
|
|
|
878
944
|
|
|
879
945
|
def _base_error_details(
|
|
880
946
|
self,
|
|
881
|
-
url:
|
|
882
|
-
) ->
|
|
947
|
+
url: "str",
|
|
948
|
+
) -> "typing.List[typing.Tuple[str, str]]":
|
|
883
949
|
return [
|
|
884
950
|
("provider", self._config.provider_name),
|
|
885
951
|
("model", self.model),
|
|
@@ -889,9 +955,9 @@ class ResponsesModelClient:
|
|
|
889
955
|
|
|
890
956
|
def _format_error_message(
|
|
891
957
|
self,
|
|
892
|
-
summary:
|
|
893
|
-
details:
|
|
894
|
-
) ->
|
|
958
|
+
summary: "str",
|
|
959
|
+
details: "typing.Iterable[typing.Tuple[str, str]]",
|
|
960
|
+
) -> "str":
|
|
895
961
|
lines = [summary]
|
|
896
962
|
for label, value in details:
|
|
897
963
|
text = str(value).strip()
|
|
@@ -902,10 +968,10 @@ class ResponsesModelClient:
|
|
|
902
968
|
|
|
903
969
|
def _format_transport_error(
|
|
904
970
|
self,
|
|
905
|
-
url:
|
|
906
|
-
exc:
|
|
907
|
-
diagnostics:
|
|
908
|
-
) ->
|
|
971
|
+
url: "str",
|
|
972
|
+
exc: "BaseException",
|
|
973
|
+
diagnostics: "typing.Union[_StreamDiagnostics, None]" = None,
|
|
974
|
+
) -> "str":
|
|
909
975
|
details = self._base_error_details(url)
|
|
910
976
|
if diagnostics is not None:
|
|
911
977
|
details.extend(self._transport_diagnostics_details(diagnostics))
|
|
@@ -940,9 +1006,9 @@ class ResponsesModelClient:
|
|
|
940
1006
|
|
|
941
1007
|
def _format_response_failed_error(
|
|
942
1008
|
self,
|
|
943
|
-
message:
|
|
944
|
-
code:
|
|
945
|
-
) ->
|
|
1009
|
+
message: "str",
|
|
1010
|
+
code: "str" = "",
|
|
1011
|
+
) -> "str":
|
|
946
1012
|
details = self._base_error_details(self.responses_url())
|
|
947
1013
|
details.append(("detail", message))
|
|
948
1014
|
if code:
|
|
@@ -961,10 +1027,10 @@ class ResponsesModelClient:
|
|
|
961
1027
|
|
|
962
1028
|
def _format_response_incomplete_error(
|
|
963
1029
|
self,
|
|
964
|
-
payload:
|
|
965
|
-
output_item_count:
|
|
966
|
-
reason:
|
|
967
|
-
) ->
|
|
1030
|
+
payload: "typing.Dict[str, object]",
|
|
1031
|
+
output_item_count: "int",
|
|
1032
|
+
reason: "typing.Union[str, None]" = None,
|
|
1033
|
+
) -> "str":
|
|
968
1034
|
details = self._base_error_details(self.responses_url())
|
|
969
1035
|
if reason:
|
|
970
1036
|
details.append(("reason", reason))
|
|
@@ -981,7 +1047,7 @@ class ResponsesModelClient:
|
|
|
981
1047
|
details,
|
|
982
1048
|
)
|
|
983
1049
|
|
|
984
|
-
def _response_incomplete_reason(self, payload:
|
|
1050
|
+
def _response_incomplete_reason(self, payload: "typing.Dict[str, object]") -> "str":
|
|
985
1051
|
response = payload.get("response")
|
|
986
1052
|
incomplete_details = (
|
|
987
1053
|
response.get("incomplete_details") if isinstance(response, dict) else None
|
|
@@ -990,7 +1056,9 @@ class ResponsesModelClient:
|
|
|
990
1056
|
return ""
|
|
991
1057
|
return str(incomplete_details.get("reason") or "").strip()
|
|
992
1058
|
|
|
993
|
-
def _raise_response_failed_error(
|
|
1059
|
+
def _raise_response_failed_error(
|
|
1060
|
+
self, payload: "typing.Dict[str, object]"
|
|
1061
|
+
) -> "None":
|
|
994
1062
|
response = payload.get("response")
|
|
995
1063
|
error = response.get("error") if isinstance(response, dict) else None
|
|
996
1064
|
if not isinstance(error, dict):
|
|
@@ -1000,10 +1068,13 @@ class ResponsesModelClient:
|
|
|
1000
1068
|
|
|
1001
1069
|
message = str(error.get("message") or "responses stream failed")
|
|
1002
1070
|
code = str(error.get("code") or error.get("type") or "").strip()
|
|
1003
|
-
if _is_context_length_error_message(
|
|
1004
|
-
|
|
1071
|
+
if code == "context_length_exceeded" or _is_context_length_error_message(
|
|
1072
|
+
message
|
|
1073
|
+
):
|
|
1074
|
+
raise ContextLengthExceeded(
|
|
1075
|
+
self._format_response_failed_error(message, code)
|
|
1076
|
+
)
|
|
1005
1077
|
if code in {
|
|
1006
|
-
"context_length_exceeded",
|
|
1007
1078
|
"insufficient_quota",
|
|
1008
1079
|
"invalid_prompt",
|
|
1009
1080
|
"model_output_invalid",
|
|
@@ -1018,9 +1089,9 @@ class ResponsesModelClient:
|
|
|
1018
1089
|
|
|
1019
1090
|
def _format_incomplete_stream_error(
|
|
1020
1091
|
self,
|
|
1021
|
-
last_event_type:
|
|
1022
|
-
output_item_count:
|
|
1023
|
-
) ->
|
|
1092
|
+
last_event_type: "str",
|
|
1093
|
+
output_item_count: "int",
|
|
1094
|
+
) -> "str":
|
|
1024
1095
|
details = self._base_error_details(self.responses_url())
|
|
1025
1096
|
if last_event_type:
|
|
1026
1097
|
details.append(("last_event", last_event_type))
|
|
@@ -1045,10 +1116,10 @@ class ResponsesModelClient:
|
|
|
1045
1116
|
|
|
1046
1117
|
def _format_invalid_event_error(
|
|
1047
1118
|
self,
|
|
1048
|
-
event_name:
|
|
1049
|
-
raw_data:
|
|
1050
|
-
exc:
|
|
1051
|
-
) ->
|
|
1119
|
+
event_name: "str",
|
|
1120
|
+
raw_data: "str",
|
|
1121
|
+
exc: "json.JSONDecodeError",
|
|
1122
|
+
) -> "str":
|
|
1052
1123
|
details = self._base_error_details(self.responses_url())
|
|
1053
1124
|
details.append(("event", event_name or "message"))
|
|
1054
1125
|
details.append(("exception", type(exc).__name__))
|
|
@@ -1062,9 +1133,9 @@ class ResponsesModelClient:
|
|
|
1062
1133
|
|
|
1063
1134
|
def _transport_diagnostics_details(
|
|
1064
1135
|
self,
|
|
1065
|
-
diagnostics:
|
|
1066
|
-
) ->
|
|
1067
|
-
details:
|
|
1136
|
+
diagnostics: "_StreamDiagnostics",
|
|
1137
|
+
) -> "typing.List[typing.Tuple[str, str]]":
|
|
1138
|
+
details: "typing.List[typing.Tuple[str, str]]" = [
|
|
1068
1139
|
("raw_lines_received", str(diagnostics.raw_lines_received)),
|
|
1069
1140
|
("sse_events_received", str(diagnostics.sse_events_received)),
|
|
1070
1141
|
("output_items_received", str(diagnostics.output_items_received)),
|
|
@@ -1077,21 +1148,21 @@ class ResponsesModelClient:
|
|
|
1077
1148
|
details.append(("last_payload_excerpt", diagnostics.last_payload_excerpt))
|
|
1078
1149
|
return details
|
|
1079
1150
|
|
|
1080
|
-
def _truncate_excerpt(self, text:
|
|
1151
|
+
def _truncate_excerpt(self, text: "str", limit: "int") -> "str":
|
|
1081
1152
|
if len(text) <= limit:
|
|
1082
1153
|
return text
|
|
1083
1154
|
return f"{text[:limit]}..."
|
|
1084
1155
|
|
|
1085
|
-
def _retry_delay_seconds(self, attempt:
|
|
1156
|
+
def _retry_delay_seconds(self, attempt: "int") -> "float":
|
|
1086
1157
|
return INITIAL_RETRY_DELAY_SECONDS * (
|
|
1087
1158
|
RETRY_BACKOFF_FACTOR ** max(min(attempt - 1, 10), 0)
|
|
1088
|
-
)
|
|
1159
|
+
) # 200s max
|
|
1089
1160
|
|
|
1090
1161
|
def _try_parse_retry_after_seconds(
|
|
1091
1162
|
self,
|
|
1092
|
-
code:
|
|
1093
|
-
message:
|
|
1094
|
-
) ->
|
|
1163
|
+
code: "str",
|
|
1164
|
+
message: "str",
|
|
1165
|
+
) -> "typing.Union[float, None]":
|
|
1095
1166
|
if code != "rate_limit_exceeded":
|
|
1096
1167
|
return None
|
|
1097
1168
|
match = RATE_LIMIT_RETRY_AFTER_RE.search(message)
|
|
@@ -1104,13 +1175,13 @@ class ResponsesModelClient:
|
|
|
1104
1175
|
return value
|
|
1105
1176
|
|
|
1106
1177
|
|
|
1107
|
-
def _optional_int(value:
|
|
1178
|
+
def _optional_int(value: "object") -> "typing.Union[int, None]":
|
|
1108
1179
|
if value is None:
|
|
1109
1180
|
return None
|
|
1110
1181
|
return int(value)
|
|
1111
1182
|
|
|
1112
1183
|
|
|
1113
|
-
def _is_context_length_error_message(message:
|
|
1184
|
+
def _is_context_length_error_message(message: "str") -> "bool":
|
|
1114
1185
|
lower = message.lower()
|
|
1115
1186
|
return (
|
|
1116
1187
|
"context_length_exceeded" in lower
|
|
@@ -1120,7 +1191,7 @@ def _is_context_length_error_message(message: 'str') -> 'bool':
|
|
|
1120
1191
|
)
|
|
1121
1192
|
|
|
1122
1193
|
|
|
1123
|
-
def _requests_verify_setting() ->
|
|
1194
|
+
def _requests_verify_setting() -> "typing.Union[typing.Union[str, bool], None]":
|
|
1124
1195
|
for env_name in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE"):
|
|
1125
1196
|
value = os.environ.get(env_name, "").strip()
|
|
1126
1197
|
if value:
|