python-codex 0.2.6__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 +18 -14
- pycodex/agent.py +468 -462
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -436
- 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 +329 -252
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +90 -52
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +235 -803
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -174
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +4 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +201 -0
- 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 +13 -13
- 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 +50 -66
- 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/utils/image_utils.py +76 -0
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +263 -161
- 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 +25 -22
- responses_server/messages_api.py +96 -49
- responses_server/payload_processors.py +25 -19
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +196 -107
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +51 -13
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +470 -384
- workspace_server/workspace.html +859 -232
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +168 -100
- 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 -553
- python_codex-0.2.6.dist-info/METADATA +0 -441
- python_codex-0.2.6.dist-info/RECORD +0 -91
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.6.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,38 +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:
|
|
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
|
|
72
95
|
|
|
73
96
|
@classmethod
|
|
74
97
|
def from_codex_config(
|
|
75
98
|
cls,
|
|
76
|
-
config_path:
|
|
77
|
-
profile:
|
|
78
|
-
) ->
|
|
99
|
+
config_path: "typing.Union[str, Path]" = DEFAULT_CODEX_CONFIG_PATH,
|
|
100
|
+
profile: "typing.Union[str, None]" = None,
|
|
101
|
+
) -> "ResponsesProviderConfig":
|
|
79
102
|
data = tomllib.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
80
103
|
selected = dict(data)
|
|
81
104
|
if profile is not None:
|
|
@@ -97,16 +120,12 @@ class ResponsesProviderConfig:
|
|
|
97
120
|
for key, value in provider.get("query_params", {}).items()
|
|
98
121
|
}
|
|
99
122
|
features = selected.get("features", {})
|
|
100
|
-
beta_features:
|
|
123
|
+
beta_features: "typing.List[str]" = []
|
|
101
124
|
if isinstance(features, dict) and features.get("guardian_approval") is True:
|
|
102
125
|
beta_features.append("guardian_approval")
|
|
103
|
-
use_chat_completion = _optional_bool(
|
|
104
|
-
selected.get("use_chat_completion")
|
|
105
|
-
)
|
|
126
|
+
use_chat_completion = _optional_bool(selected.get("use_chat_completion"))
|
|
106
127
|
if use_chat_completion is None:
|
|
107
|
-
use_chat_completion = _optional_bool(
|
|
108
|
-
provider.get("use_chat_completion")
|
|
109
|
-
)
|
|
128
|
+
use_chat_completion = _optional_bool(provider.get("use_chat_completion"))
|
|
110
129
|
if use_chat_completion is None:
|
|
111
130
|
use_chat_completion = False
|
|
112
131
|
return cls(
|
|
@@ -124,10 +143,12 @@ class ResponsesProviderConfig:
|
|
|
124
143
|
sandbox_mode=selected.get("sandbox_mode"),
|
|
125
144
|
beta_features_header=",".join(beta_features) or None,
|
|
126
145
|
stream_max_retries=_optional_int(provider.get("stream_max_retries")),
|
|
127
|
-
stream_idle_timeout_ms=_optional_int(
|
|
146
|
+
stream_idle_timeout_ms=_optional_int(
|
|
147
|
+
provider.get("stream_idle_timeout_ms")
|
|
148
|
+
),
|
|
128
149
|
)
|
|
129
150
|
|
|
130
|
-
def api_key(self) ->
|
|
151
|
+
def api_key(self) -> "typing.Union[str, None]":
|
|
131
152
|
if not self.api_key_env:
|
|
132
153
|
return None
|
|
133
154
|
value = os.environ.get(self.api_key_env, "")
|
|
@@ -139,39 +160,39 @@ class ResponsesProviderConfig:
|
|
|
139
160
|
|
|
140
161
|
def with_overrides(
|
|
141
162
|
self,
|
|
142
|
-
model:
|
|
143
|
-
reasoning_effort:
|
|
144
|
-
) ->
|
|
163
|
+
model: "typing.Union[str, None]" = None,
|
|
164
|
+
reasoning_effort: "typing.Union[str, None]" = None,
|
|
165
|
+
) -> "ResponsesProviderConfig":
|
|
145
166
|
return replace(
|
|
146
167
|
self,
|
|
147
168
|
model=self.model if model is None else model,
|
|
148
169
|
reasoning_effort=(
|
|
149
|
-
self.reasoning_effort
|
|
150
|
-
if reasoning_effort is None
|
|
151
|
-
else reasoning_effort
|
|
170
|
+
self.reasoning_effort if reasoning_effort is None else reasoning_effort
|
|
152
171
|
),
|
|
153
172
|
)
|
|
154
173
|
|
|
155
|
-
def effective_stream_max_retries(self) ->
|
|
174
|
+
def effective_stream_max_retries(self) -> "int":
|
|
156
175
|
if self.stream_max_retries is None:
|
|
157
176
|
return DEFAULT_STREAM_MAX_RETRIES
|
|
158
177
|
return max(int(self.stream_max_retries), 0)
|
|
159
178
|
|
|
160
|
-
def effective_stream_idle_timeout_seconds(self) ->
|
|
179
|
+
def effective_stream_idle_timeout_seconds(self) -> "float":
|
|
161
180
|
if self.stream_idle_timeout_ms is None:
|
|
162
181
|
return DEFAULT_STREAM_IDLE_TIMEOUT_MS / 1000.0
|
|
163
182
|
return max(int(self.stream_idle_timeout_ms), 1) / 1000.0
|
|
164
183
|
|
|
165
|
-
def metadata(self) ->
|
|
184
|
+
def metadata(self) -> "typing.Union[JSONDict, None]":
|
|
166
185
|
return model_metadata(self.model)
|
|
167
186
|
|
|
168
|
-
def use_responses_lite(self) ->
|
|
187
|
+
def use_responses_lite(self) -> "bool":
|
|
188
|
+
if self.responses_lite_override is not None:
|
|
189
|
+
return self.responses_lite_override
|
|
169
190
|
metadata = self.metadata()
|
|
170
191
|
if metadata is None:
|
|
171
192
|
return False
|
|
172
193
|
return metadata.get("use_responses_lite") is True
|
|
173
194
|
|
|
174
|
-
def effective_reasoning_effort(self) ->
|
|
195
|
+
def effective_reasoning_effort(self) -> "typing.Union[str, None]":
|
|
175
196
|
if self.reasoning_effort is not None:
|
|
176
197
|
return str(self.reasoning_effort)
|
|
177
198
|
metadata = self.metadata()
|
|
@@ -179,7 +200,7 @@ class ResponsesProviderConfig:
|
|
|
179
200
|
return None
|
|
180
201
|
return _optional_metadata_string(metadata, "default_reasoning_level")
|
|
181
202
|
|
|
182
|
-
def effective_reasoning_summary(self) ->
|
|
203
|
+
def effective_reasoning_summary(self) -> "typing.Union[str, None]":
|
|
183
204
|
summary = self.reasoning_summary
|
|
184
205
|
if summary is None:
|
|
185
206
|
metadata = self.metadata()
|
|
@@ -192,7 +213,7 @@ class ResponsesProviderConfig:
|
|
|
192
213
|
return None
|
|
193
214
|
return str(summary)
|
|
194
215
|
|
|
195
|
-
def effective_verbosity(self) ->
|
|
216
|
+
def effective_verbosity(self) -> "typing.Union[str, None]":
|
|
196
217
|
if self.verbosity is not None:
|
|
197
218
|
return str(self.verbosity)
|
|
198
219
|
metadata = self.metadata()
|
|
@@ -200,7 +221,7 @@ class ResponsesProviderConfig:
|
|
|
200
221
|
return None
|
|
201
222
|
return _optional_metadata_string(metadata, "default_verbosity")
|
|
202
223
|
|
|
203
|
-
def effective_service_tier(self) ->
|
|
224
|
+
def effective_service_tier(self) -> "typing.Union[str, None]":
|
|
204
225
|
service_tier = self.service_tier
|
|
205
226
|
if service_tier is None:
|
|
206
227
|
return None
|
|
@@ -222,7 +243,9 @@ class ResponsesProviderConfig:
|
|
|
222
243
|
return None
|
|
223
244
|
|
|
224
245
|
|
|
225
|
-
def _optional_bool(
|
|
246
|
+
def _optional_bool(
|
|
247
|
+
value: "typing.Union[bool, str, int, None]",
|
|
248
|
+
) -> "typing.Union[bool, None]":
|
|
226
249
|
if value is None:
|
|
227
250
|
return None
|
|
228
251
|
if isinstance(value, bool):
|
|
@@ -236,15 +259,15 @@ def _optional_bool(value: 'typing.Union[bool, str, int, None]') -> 'typing.Union
|
|
|
236
259
|
|
|
237
260
|
|
|
238
261
|
def _metadata_supports_reasoning(
|
|
239
|
-
metadata:
|
|
240
|
-
) ->
|
|
262
|
+
metadata: "typing.Union[JSONDict, None]",
|
|
263
|
+
) -> "bool":
|
|
241
264
|
return metadata is not None and metadata.get("supports_reasoning_summaries") is True
|
|
242
265
|
|
|
243
266
|
|
|
244
267
|
def _optional_metadata_string(
|
|
245
|
-
metadata:
|
|
246
|
-
key:
|
|
247
|
-
) ->
|
|
268
|
+
metadata: "typing.Union[JSONDict, None]",
|
|
269
|
+
key: "str",
|
|
270
|
+
) -> "typing.Union[str, None]":
|
|
248
271
|
if metadata is None:
|
|
249
272
|
return None
|
|
250
273
|
value = metadata.get(key)
|
|
@@ -254,7 +277,7 @@ def _optional_metadata_string(
|
|
|
254
277
|
return text or None
|
|
255
278
|
|
|
256
279
|
|
|
257
|
-
def _strip_image_details(items:
|
|
280
|
+
def _strip_image_details(items: "typing.Iterable[object]") -> "None":
|
|
258
281
|
for item in items:
|
|
259
282
|
if not isinstance(item, dict):
|
|
260
283
|
continue
|
|
@@ -266,7 +289,7 @@ def _strip_image_details(items: 'typing.Iterable[object]') -> 'None':
|
|
|
266
289
|
_strip_image_detail_from_content_items(output)
|
|
267
290
|
|
|
268
291
|
|
|
269
|
-
def _strip_image_detail_from_content_items(items:
|
|
292
|
+
def _strip_image_detail_from_content_items(items: "typing.Iterable[object]") -> "None":
|
|
270
293
|
for item in items:
|
|
271
294
|
if isinstance(item, dict) and item.get("type") == "input_image":
|
|
272
295
|
item.pop("detail", None)
|
|
@@ -276,13 +299,39 @@ class ResponsesApiError(RuntimeError):
|
|
|
276
299
|
pass
|
|
277
300
|
|
|
278
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
|
+
|
|
279
328
|
class ResponsesIncompleteError(ResponsesApiError):
|
|
280
329
|
def __init__(
|
|
281
330
|
self,
|
|
282
|
-
message:
|
|
283
|
-
partial_items:
|
|
284
|
-
reason:
|
|
285
|
-
) ->
|
|
331
|
+
message: "str",
|
|
332
|
+
partial_items: "typing.Sequence[ModelOutputItem]",
|
|
333
|
+
reason: "str" = "",
|
|
334
|
+
) -> "None":
|
|
286
335
|
super().__init__(message)
|
|
287
336
|
self.partial_items = tuple(partial_items)
|
|
288
337
|
self.reason = reason
|
|
@@ -291,21 +340,21 @@ class ResponsesIncompleteError(ResponsesApiError):
|
|
|
291
340
|
class ResponsesRetryableError(ResponsesApiError):
|
|
292
341
|
def __init__(
|
|
293
342
|
self,
|
|
294
|
-
message:
|
|
295
|
-
retry_delay_seconds:
|
|
296
|
-
) ->
|
|
343
|
+
message: "str",
|
|
344
|
+
retry_delay_seconds: "typing.Union[float, None]" = None,
|
|
345
|
+
) -> "None":
|
|
297
346
|
super().__init__(message)
|
|
298
347
|
self.retry_delay_seconds = retry_delay_seconds
|
|
299
348
|
|
|
300
349
|
|
|
301
350
|
@dataclass
|
|
302
351
|
class _StreamDiagnostics:
|
|
303
|
-
raw_lines_received:
|
|
304
|
-
sse_events_received:
|
|
305
|
-
output_items_received:
|
|
306
|
-
last_sse_event_name:
|
|
307
|
-
last_event_type:
|
|
308
|
-
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" = ""
|
|
309
358
|
|
|
310
359
|
|
|
311
360
|
class ResponsesModelClient:
|
|
@@ -318,40 +367,49 @@ class ResponsesModelClient:
|
|
|
318
367
|
|
|
319
368
|
def __init__(
|
|
320
369
|
self,
|
|
321
|
-
config:
|
|
322
|
-
timeout_seconds:
|
|
323
|
-
session_id:
|
|
324
|
-
originator:
|
|
325
|
-
user_agent:
|
|
326
|
-
openai_subagent:
|
|
327
|
-
) ->
|
|
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":
|
|
328
377
|
self._config = config
|
|
329
|
-
self.model = config.model
|
|
330
378
|
self._timeout_seconds = timeout_seconds
|
|
331
379
|
self._session_id = session_id or uuid7_string()
|
|
332
380
|
self._originator = originator
|
|
333
381
|
self._user_agent = user_agent or build_user_agent(originator)
|
|
334
382
|
self._openai_subagent = openai_subagent
|
|
335
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
|
+
|
|
336
392
|
@classmethod
|
|
337
393
|
def from_codex_config(
|
|
338
394
|
cls,
|
|
339
|
-
config_path:
|
|
340
|
-
profile:
|
|
341
|
-
timeout_seconds:
|
|
342
|
-
originator:
|
|
343
|
-
user_agent:
|
|
344
|
-
) ->
|
|
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":
|
|
345
401
|
config = ResponsesProviderConfig.from_codex_config(config_path, profile)
|
|
346
|
-
return cls(
|
|
402
|
+
return cls(
|
|
403
|
+
config, timeout_seconds, originator=originator, user_agent=user_agent
|
|
404
|
+
)
|
|
347
405
|
|
|
348
406
|
def with_overrides(
|
|
349
407
|
self,
|
|
350
|
-
model:
|
|
351
|
-
reasoning_effort:
|
|
352
|
-
session_id:
|
|
353
|
-
openai_subagent:
|
|
354
|
-
) ->
|
|
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":
|
|
355
413
|
return ResponsesModelClient(
|
|
356
414
|
self._config.with_overrides(
|
|
357
415
|
model or self.model,
|
|
@@ -362,79 +420,85 @@ class ResponsesModelClient:
|
|
|
362
420
|
originator=self._originator,
|
|
363
421
|
user_agent=self._user_agent,
|
|
364
422
|
openai_subagent=(
|
|
365
|
-
self._openai_subagent
|
|
366
|
-
if openai_subagent is None
|
|
367
|
-
else openai_subagent
|
|
423
|
+
self._openai_subagent if openai_subagent is None else openai_subagent
|
|
368
424
|
),
|
|
369
425
|
)
|
|
370
426
|
|
|
371
|
-
def responses_url(self) ->
|
|
427
|
+
def responses_url(self) -> "str":
|
|
372
428
|
base_url = self._config.base_url.rstrip("/")
|
|
373
429
|
url = f"{base_url}/responses"
|
|
374
430
|
if self._config.query_params:
|
|
375
431
|
return f"{url}?{urllib.parse.urlencode(self._config.query_params)}"
|
|
376
432
|
return url
|
|
377
433
|
|
|
378
|
-
def models_url(self) ->
|
|
434
|
+
def models_url(self) -> "str":
|
|
379
435
|
base_url = self._config.base_url.rstrip("/")
|
|
380
436
|
url = f"{base_url}/models"
|
|
381
437
|
if self._config.query_params:
|
|
382
438
|
return f"{url}?{urllib.parse.urlencode(self._config.query_params)}"
|
|
383
439
|
return url
|
|
384
440
|
|
|
385
|
-
async def list_models(self) ->
|
|
386
|
-
return await asyncio.to_thread(self.
|
|
441
|
+
async def list_models(self) -> "typing.List[str]":
|
|
442
|
+
return await asyncio.to_thread(self.list_models_sync)
|
|
443
|
+
|
|
444
|
+
def list_models_sync(self) -> "typing.List[str]":
|
|
445
|
+
return self._list_models_sync()
|
|
387
446
|
|
|
388
447
|
async def complete(
|
|
389
448
|
self,
|
|
390
|
-
prompt:
|
|
391
|
-
event_handler:
|
|
392
|
-
) ->
|
|
449
|
+
prompt: "Prompt",
|
|
450
|
+
event_handler: "ModelStreamEventHandler" = NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
451
|
+
) -> "ModelResponse":
|
|
393
452
|
retries = 0
|
|
394
453
|
max_retries = self._config.effective_stream_max_retries()
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
if delay_seconds is None:
|
|
417
|
-
delay_seconds = self._retry_delay_seconds(retries)
|
|
418
|
-
event_handler(
|
|
419
|
-
ModelStreamEvent(
|
|
420
|
-
kind="stream_error",
|
|
421
|
-
payload={
|
|
422
|
-
"message": f"Reconnecting... {retries}/{max_retries}",
|
|
423
|
-
"attempt": retries,
|
|
424
|
-
"max_retries": max_retries,
|
|
425
|
-
"delay_seconds": delay_seconds,
|
|
426
|
-
"error": str(exc),
|
|
427
|
-
},
|
|
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,
|
|
428
474
|
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
432
496
|
|
|
433
497
|
def _complete_sync(
|
|
434
498
|
self,
|
|
435
|
-
prompt:
|
|
436
|
-
event_handler:
|
|
437
|
-
) ->
|
|
499
|
+
prompt: "Prompt",
|
|
500
|
+
event_handler: "ModelStreamEventHandler",
|
|
501
|
+
) -> "ModelResponse":
|
|
438
502
|
payload = self._build_payload(prompt)
|
|
439
503
|
body = json.dumps(payload).encode("utf-8")
|
|
440
504
|
url = self.responses_url()
|
|
@@ -475,6 +539,8 @@ class ResponsesModelClient:
|
|
|
475
539
|
f"responses request failed with status {response.status_code}: "
|
|
476
540
|
f"{error_body[:500]}"
|
|
477
541
|
)
|
|
542
|
+
if _is_context_length_error_message(error_body):
|
|
543
|
+
raise ContextLengthExceeded(message)
|
|
478
544
|
if response.status_code >= 500:
|
|
479
545
|
raise ResponsesRetryableError(message)
|
|
480
546
|
raise ResponsesApiError(message)
|
|
@@ -492,26 +558,37 @@ class ResponsesModelClient:
|
|
|
492
558
|
self._format_transport_error(url, exc, diagnostics)
|
|
493
559
|
) from exc
|
|
494
560
|
|
|
495
|
-
def _build_payload(self, prompt:
|
|
561
|
+
def _build_payload(self, prompt: "Prompt") -> "typing.Dict[str, object]":
|
|
496
562
|
use_responses_lite = self._config.use_responses_lite()
|
|
497
563
|
input_items = [item.serialize() for item in prompt.input]
|
|
498
564
|
if use_responses_lite:
|
|
499
565
|
_strip_image_details(input_items)
|
|
500
566
|
|
|
501
567
|
tools = [tool.serialize() for tool in prompt.tools]
|
|
502
|
-
payload:
|
|
568
|
+
payload: "typing.Dict[str, object]" = {
|
|
503
569
|
"model": self.model,
|
|
504
570
|
"input": input_items,
|
|
505
|
-
"parallel_tool_calls": prompt.parallel_tool_calls
|
|
571
|
+
"parallel_tool_calls": prompt.parallel_tool_calls
|
|
572
|
+
and not use_responses_lite,
|
|
506
573
|
"store": False,
|
|
507
574
|
"stream": True,
|
|
508
575
|
"include": ["reasoning.encrypted_content"],
|
|
509
576
|
"prompt_cache_key": self._session_id,
|
|
510
577
|
}
|
|
511
578
|
if use_responses_lite:
|
|
512
|
-
|
|
579
|
+
prefix_namespace = uuid.uuid5(uuid.NAMESPACE_OID, self._session_id)
|
|
580
|
+
prefix: "typing.List[typing.Dict[str, object]]" = [
|
|
513
581
|
{
|
|
514
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
|
+
),
|
|
515
592
|
"role": "developer",
|
|
516
593
|
"tools": tools,
|
|
517
594
|
}
|
|
@@ -520,6 +597,8 @@ class ResponsesModelClient:
|
|
|
520
597
|
prefix.append(
|
|
521
598
|
{
|
|
522
599
|
"type": "message",
|
|
600
|
+
"id": "msg_"
|
|
601
|
+
+ str(uuid.uuid5(prefix_namespace, prompt.base_instructions)),
|
|
523
602
|
"role": "developer",
|
|
524
603
|
"content": [
|
|
525
604
|
{
|
|
@@ -537,7 +616,7 @@ class ResponsesModelClient:
|
|
|
537
616
|
if prompt.tools or use_responses_lite:
|
|
538
617
|
payload["tool_choice"] = "auto"
|
|
539
618
|
|
|
540
|
-
reasoning:
|
|
619
|
+
reasoning: "typing.Dict[str, str]" = {}
|
|
541
620
|
reasoning_effort = self._config.effective_reasoning_effort()
|
|
542
621
|
reasoning_summary = self._config.effective_reasoning_summary()
|
|
543
622
|
if reasoning_effort is not None:
|
|
@@ -562,7 +641,7 @@ class ResponsesModelClient:
|
|
|
562
641
|
|
|
563
642
|
return payload
|
|
564
643
|
|
|
565
|
-
def _list_models_sync(self) ->
|
|
644
|
+
def _list_models_sync(self) -> "typing.List[str]":
|
|
566
645
|
prepared = requests.PreparedRequest()
|
|
567
646
|
prepared.prepare(
|
|
568
647
|
method="GET",
|
|
@@ -600,7 +679,7 @@ class ResponsesModelClient:
|
|
|
600
679
|
data = payload.get("data")
|
|
601
680
|
if not isinstance(data, list):
|
|
602
681
|
raise ResponsesApiError("models response is missing `data` list")
|
|
603
|
-
models:
|
|
682
|
+
models: "typing.List[str]" = []
|
|
604
683
|
for item in data:
|
|
605
684
|
if not isinstance(item, dict):
|
|
606
685
|
continue
|
|
@@ -609,7 +688,7 @@ class ResponsesModelClient:
|
|
|
609
688
|
models.append(model_id)
|
|
610
689
|
return models
|
|
611
690
|
|
|
612
|
-
def _build_headers(self, prompt:
|
|
691
|
+
def _build_headers(self, prompt: "Prompt") -> "typing.Dict[str, str]":
|
|
613
692
|
headers = {
|
|
614
693
|
"content-type": "application/json",
|
|
615
694
|
"accept": "text/event-stream",
|
|
@@ -634,7 +713,7 @@ class ResponsesModelClient:
|
|
|
634
713
|
)
|
|
635
714
|
return headers
|
|
636
715
|
|
|
637
|
-
def _build_model_list_headers(self) ->
|
|
716
|
+
def _build_model_list_headers(self) -> "typing.Dict[str, str]":
|
|
638
717
|
headers = {
|
|
639
718
|
"accept": "application/json",
|
|
640
719
|
"originator": self._originator,
|
|
@@ -652,10 +731,10 @@ class ResponsesModelClient:
|
|
|
652
731
|
def _parse_stream(
|
|
653
732
|
self,
|
|
654
733
|
response,
|
|
655
|
-
event_handler:
|
|
656
|
-
diagnostics:
|
|
657
|
-
) ->
|
|
658
|
-
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]]" = ([])
|
|
659
738
|
saw_completed = False
|
|
660
739
|
last_event_type = ""
|
|
661
740
|
|
|
@@ -674,12 +753,7 @@ class ResponsesModelClient:
|
|
|
674
753
|
diagnostics.last_event_type = last_event_type
|
|
675
754
|
|
|
676
755
|
if event_type == "response.output_text.delta":
|
|
677
|
-
event_handler(
|
|
678
|
-
ModelStreamEvent(
|
|
679
|
-
kind="assistant_delta",
|
|
680
|
-
payload={"delta": str(payload.get("delta", ""))},
|
|
681
|
-
)
|
|
682
|
-
)
|
|
756
|
+
event_handler(AssistantDeltaEvent(str(payload.get("delta", ""))))
|
|
683
757
|
continue
|
|
684
758
|
|
|
685
759
|
if event_type == "response.output_item.done":
|
|
@@ -689,31 +763,41 @@ class ResponsesModelClient:
|
|
|
689
763
|
and item_payload.get("type") == "web_search_call"
|
|
690
764
|
):
|
|
691
765
|
action_payload = item_payload.get("action")
|
|
692
|
-
|
|
693
|
-
"call_id": str(item_payload.get("id", "web_search")),
|
|
694
|
-
"tool_name": "web_search",
|
|
695
|
-
}
|
|
766
|
+
action_type = None
|
|
696
767
|
if isinstance(action_payload, dict):
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
event_payload["query"] = str(action_payload.get("query", ""))
|
|
702
|
-
queries = action_payload.get("queries")
|
|
703
|
-
if isinstance(queries, list):
|
|
704
|
-
event_payload["queries"] = [
|
|
705
|
-
str(query) for query in queries if str(query).strip()
|
|
706
|
-
]
|
|
707
|
-
if "url" in action_payload:
|
|
708
|
-
event_payload["url"] = str(action_payload.get("url", ""))
|
|
709
|
-
if "pattern" in action_payload:
|
|
710
|
-
event_payload["pattern"] = str(
|
|
711
|
-
action_payload.get("pattern", "")
|
|
712
|
-
)
|
|
768
|
+
action_type = str(action_payload.get("type", ""))
|
|
769
|
+
else:
|
|
770
|
+
action_payload = {}
|
|
771
|
+
queries = action_payload.get("queries")
|
|
713
772
|
event_handler(
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
+
),
|
|
717
801
|
)
|
|
718
802
|
)
|
|
719
803
|
continue
|
|
@@ -721,15 +805,7 @@ class ResponsesModelClient:
|
|
|
721
805
|
parsed = self._parse_output_item(item_payload)
|
|
722
806
|
if parsed is not None:
|
|
723
807
|
if isinstance(parsed, ToolCall):
|
|
724
|
-
event_handler(
|
|
725
|
-
ModelStreamEvent(
|
|
726
|
-
kind="tool_call",
|
|
727
|
-
payload={
|
|
728
|
-
"call_id": parsed.call_id,
|
|
729
|
-
"tool_name": parsed.name,
|
|
730
|
-
},
|
|
731
|
-
)
|
|
732
|
-
)
|
|
808
|
+
event_handler(ToolCalledEvent(parsed.call_id, parsed.name))
|
|
733
809
|
items.append(parsed)
|
|
734
810
|
if diagnostics is not None:
|
|
735
811
|
diagnostics.output_items_received += 1
|
|
@@ -744,12 +820,7 @@ class ResponsesModelClient:
|
|
|
744
820
|
usage = dict(response_usage)
|
|
745
821
|
elif isinstance(payload.get("usage"), dict):
|
|
746
822
|
usage = dict(payload["usage"])
|
|
747
|
-
event_handler(
|
|
748
|
-
ModelStreamEvent(
|
|
749
|
-
kind="token_count",
|
|
750
|
-
payload={"usage": usage},
|
|
751
|
-
)
|
|
752
|
-
)
|
|
823
|
+
event_handler(TokenCountEvent(usage))
|
|
753
824
|
saw_completed = True
|
|
754
825
|
break
|
|
755
826
|
|
|
@@ -778,22 +849,17 @@ class ResponsesModelClient:
|
|
|
778
849
|
|
|
779
850
|
def _parse_output_item(
|
|
780
851
|
self,
|
|
781
|
-
item:
|
|
782
|
-
) ->
|
|
852
|
+
item: "typing.Dict[str, object]",
|
|
853
|
+
) -> "typing.Union[typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], None]":
|
|
783
854
|
item_type = item.get("type")
|
|
784
855
|
if item_type == "reasoning":
|
|
785
856
|
return ReasoningItem(payload=dict(item))
|
|
786
857
|
|
|
787
858
|
if item_type == "message" and item.get("role") == "assistant":
|
|
788
|
-
|
|
789
|
-
text_parts = []
|
|
790
|
-
for part in content:
|
|
791
|
-
if isinstance(part, dict) and part.get("type") == "output_text":
|
|
792
|
-
text_parts.append(str(part.get("text", "")))
|
|
793
|
-
return AssistantMessage(text="".join(text_parts))
|
|
859
|
+
return AssistantMessage.from_response_item(item)
|
|
794
860
|
|
|
795
861
|
if item_type == "function_call":
|
|
796
|
-
raw_arguments =
|
|
862
|
+
raw_arguments = item["arguments"]
|
|
797
863
|
arguments = json.loads(raw_arguments)
|
|
798
864
|
if not isinstance(arguments, dict):
|
|
799
865
|
raise ResponsesApiError(
|
|
@@ -803,6 +869,9 @@ class ResponsesModelClient:
|
|
|
803
869
|
call_id=str(item["call_id"]),
|
|
804
870
|
name=str(item["name"]),
|
|
805
871
|
arguments=arguments,
|
|
872
|
+
id=item.get("id"),
|
|
873
|
+
raw_arguments=raw_arguments,
|
|
874
|
+
namespace=item.get("namespace"),
|
|
806
875
|
)
|
|
807
876
|
|
|
808
877
|
if item_type == "custom_tool_call":
|
|
@@ -811,6 +880,9 @@ class ResponsesModelClient:
|
|
|
811
880
|
name=str(item["name"]),
|
|
812
881
|
arguments=str(item.get("input", "")),
|
|
813
882
|
tool_type="custom",
|
|
883
|
+
id=item.get("id"),
|
|
884
|
+
namespace=item.get("namespace"),
|
|
885
|
+
status=item.get("status"),
|
|
814
886
|
)
|
|
815
887
|
|
|
816
888
|
return None
|
|
@@ -818,10 +890,10 @@ class ResponsesModelClient:
|
|
|
818
890
|
def _iter_sse_events(
|
|
819
891
|
self,
|
|
820
892
|
response,
|
|
821
|
-
diagnostics:
|
|
893
|
+
diagnostics: "typing.Union[_StreamDiagnostics, None]" = None,
|
|
822
894
|
):
|
|
823
|
-
event_name:
|
|
824
|
-
data_lines:
|
|
895
|
+
event_name: "typing.Union[str, None]" = None
|
|
896
|
+
data_lines: "typing.List[str]" = []
|
|
825
897
|
|
|
826
898
|
for raw_line in response:
|
|
827
899
|
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|
|
@@ -864,7 +936,7 @@ class ResponsesModelClient:
|
|
|
864
936
|
def _track_stream_lines(
|
|
865
937
|
self,
|
|
866
938
|
response,
|
|
867
|
-
diagnostics:
|
|
939
|
+
diagnostics: "_StreamDiagnostics",
|
|
868
940
|
):
|
|
869
941
|
for raw_line in response:
|
|
870
942
|
diagnostics.raw_lines_received += 1
|
|
@@ -872,8 +944,8 @@ class ResponsesModelClient:
|
|
|
872
944
|
|
|
873
945
|
def _base_error_details(
|
|
874
946
|
self,
|
|
875
|
-
url:
|
|
876
|
-
) ->
|
|
947
|
+
url: "str",
|
|
948
|
+
) -> "typing.List[typing.Tuple[str, str]]":
|
|
877
949
|
return [
|
|
878
950
|
("provider", self._config.provider_name),
|
|
879
951
|
("model", self.model),
|
|
@@ -883,9 +955,9 @@ class ResponsesModelClient:
|
|
|
883
955
|
|
|
884
956
|
def _format_error_message(
|
|
885
957
|
self,
|
|
886
|
-
summary:
|
|
887
|
-
details:
|
|
888
|
-
) ->
|
|
958
|
+
summary: "str",
|
|
959
|
+
details: "typing.Iterable[typing.Tuple[str, str]]",
|
|
960
|
+
) -> "str":
|
|
889
961
|
lines = [summary]
|
|
890
962
|
for label, value in details:
|
|
891
963
|
text = str(value).strip()
|
|
@@ -896,10 +968,10 @@ class ResponsesModelClient:
|
|
|
896
968
|
|
|
897
969
|
def _format_transport_error(
|
|
898
970
|
self,
|
|
899
|
-
url:
|
|
900
|
-
exc:
|
|
901
|
-
diagnostics:
|
|
902
|
-
) ->
|
|
971
|
+
url: "str",
|
|
972
|
+
exc: "BaseException",
|
|
973
|
+
diagnostics: "typing.Union[_StreamDiagnostics, None]" = None,
|
|
974
|
+
) -> "str":
|
|
903
975
|
details = self._base_error_details(url)
|
|
904
976
|
if diagnostics is not None:
|
|
905
977
|
details.extend(self._transport_diagnostics_details(diagnostics))
|
|
@@ -934,9 +1006,9 @@ class ResponsesModelClient:
|
|
|
934
1006
|
|
|
935
1007
|
def _format_response_failed_error(
|
|
936
1008
|
self,
|
|
937
|
-
message:
|
|
938
|
-
code:
|
|
939
|
-
) ->
|
|
1009
|
+
message: "str",
|
|
1010
|
+
code: "str" = "",
|
|
1011
|
+
) -> "str":
|
|
940
1012
|
details = self._base_error_details(self.responses_url())
|
|
941
1013
|
details.append(("detail", message))
|
|
942
1014
|
if code:
|
|
@@ -955,10 +1027,10 @@ class ResponsesModelClient:
|
|
|
955
1027
|
|
|
956
1028
|
def _format_response_incomplete_error(
|
|
957
1029
|
self,
|
|
958
|
-
payload:
|
|
959
|
-
output_item_count:
|
|
960
|
-
reason:
|
|
961
|
-
) ->
|
|
1030
|
+
payload: "typing.Dict[str, object]",
|
|
1031
|
+
output_item_count: "int",
|
|
1032
|
+
reason: "typing.Union[str, None]" = None,
|
|
1033
|
+
) -> "str":
|
|
962
1034
|
details = self._base_error_details(self.responses_url())
|
|
963
1035
|
if reason:
|
|
964
1036
|
details.append(("reason", reason))
|
|
@@ -975,7 +1047,7 @@ class ResponsesModelClient:
|
|
|
975
1047
|
details,
|
|
976
1048
|
)
|
|
977
1049
|
|
|
978
|
-
def _response_incomplete_reason(self, payload:
|
|
1050
|
+
def _response_incomplete_reason(self, payload: "typing.Dict[str, object]") -> "str":
|
|
979
1051
|
response = payload.get("response")
|
|
980
1052
|
incomplete_details = (
|
|
981
1053
|
response.get("incomplete_details") if isinstance(response, dict) else None
|
|
@@ -984,7 +1056,9 @@ class ResponsesModelClient:
|
|
|
984
1056
|
return ""
|
|
985
1057
|
return str(incomplete_details.get("reason") or "").strip()
|
|
986
1058
|
|
|
987
|
-
def _raise_response_failed_error(
|
|
1059
|
+
def _raise_response_failed_error(
|
|
1060
|
+
self, payload: "typing.Dict[str, object]"
|
|
1061
|
+
) -> "None":
|
|
988
1062
|
response = payload.get("response")
|
|
989
1063
|
error = response.get("error") if isinstance(response, dict) else None
|
|
990
1064
|
if not isinstance(error, dict):
|
|
@@ -994,10 +1068,13 @@ class ResponsesModelClient:
|
|
|
994
1068
|
|
|
995
1069
|
message = str(error.get("message") or "responses stream failed")
|
|
996
1070
|
code = str(error.get("code") or error.get("type") or "").strip()
|
|
997
|
-
if _is_context_length_error_message(
|
|
998
|
-
|
|
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
|
+
)
|
|
999
1077
|
if code in {
|
|
1000
|
-
"context_length_exceeded",
|
|
1001
1078
|
"insufficient_quota",
|
|
1002
1079
|
"invalid_prompt",
|
|
1003
1080
|
"model_output_invalid",
|
|
@@ -1012,9 +1089,9 @@ class ResponsesModelClient:
|
|
|
1012
1089
|
|
|
1013
1090
|
def _format_incomplete_stream_error(
|
|
1014
1091
|
self,
|
|
1015
|
-
last_event_type:
|
|
1016
|
-
output_item_count:
|
|
1017
|
-
) ->
|
|
1092
|
+
last_event_type: "str",
|
|
1093
|
+
output_item_count: "int",
|
|
1094
|
+
) -> "str":
|
|
1018
1095
|
details = self._base_error_details(self.responses_url())
|
|
1019
1096
|
if last_event_type:
|
|
1020
1097
|
details.append(("last_event", last_event_type))
|
|
@@ -1039,10 +1116,10 @@ class ResponsesModelClient:
|
|
|
1039
1116
|
|
|
1040
1117
|
def _format_invalid_event_error(
|
|
1041
1118
|
self,
|
|
1042
|
-
event_name:
|
|
1043
|
-
raw_data:
|
|
1044
|
-
exc:
|
|
1045
|
-
) ->
|
|
1119
|
+
event_name: "str",
|
|
1120
|
+
raw_data: "str",
|
|
1121
|
+
exc: "json.JSONDecodeError",
|
|
1122
|
+
) -> "str":
|
|
1046
1123
|
details = self._base_error_details(self.responses_url())
|
|
1047
1124
|
details.append(("event", event_name or "message"))
|
|
1048
1125
|
details.append(("exception", type(exc).__name__))
|
|
@@ -1056,9 +1133,9 @@ class ResponsesModelClient:
|
|
|
1056
1133
|
|
|
1057
1134
|
def _transport_diagnostics_details(
|
|
1058
1135
|
self,
|
|
1059
|
-
diagnostics:
|
|
1060
|
-
) ->
|
|
1061
|
-
details:
|
|
1136
|
+
diagnostics: "_StreamDiagnostics",
|
|
1137
|
+
) -> "typing.List[typing.Tuple[str, str]]":
|
|
1138
|
+
details: "typing.List[typing.Tuple[str, str]]" = [
|
|
1062
1139
|
("raw_lines_received", str(diagnostics.raw_lines_received)),
|
|
1063
1140
|
("sse_events_received", str(diagnostics.sse_events_received)),
|
|
1064
1141
|
("output_items_received", str(diagnostics.output_items_received)),
|
|
@@ -1071,21 +1148,21 @@ class ResponsesModelClient:
|
|
|
1071
1148
|
details.append(("last_payload_excerpt", diagnostics.last_payload_excerpt))
|
|
1072
1149
|
return details
|
|
1073
1150
|
|
|
1074
|
-
def _truncate_excerpt(self, text:
|
|
1151
|
+
def _truncate_excerpt(self, text: "str", limit: "int") -> "str":
|
|
1075
1152
|
if len(text) <= limit:
|
|
1076
1153
|
return text
|
|
1077
1154
|
return f"{text[:limit]}..."
|
|
1078
1155
|
|
|
1079
|
-
def _retry_delay_seconds(self, attempt:
|
|
1156
|
+
def _retry_delay_seconds(self, attempt: "int") -> "float":
|
|
1080
1157
|
return INITIAL_RETRY_DELAY_SECONDS * (
|
|
1081
1158
|
RETRY_BACKOFF_FACTOR ** max(min(attempt - 1, 10), 0)
|
|
1082
|
-
)
|
|
1159
|
+
) # 200s max
|
|
1083
1160
|
|
|
1084
1161
|
def _try_parse_retry_after_seconds(
|
|
1085
1162
|
self,
|
|
1086
|
-
code:
|
|
1087
|
-
message:
|
|
1088
|
-
) ->
|
|
1163
|
+
code: "str",
|
|
1164
|
+
message: "str",
|
|
1165
|
+
) -> "typing.Union[float, None]":
|
|
1089
1166
|
if code != "rate_limit_exceeded":
|
|
1090
1167
|
return None
|
|
1091
1168
|
match = RATE_LIMIT_RETRY_AFTER_RE.search(message)
|
|
@@ -1098,13 +1175,13 @@ class ResponsesModelClient:
|
|
|
1098
1175
|
return value
|
|
1099
1176
|
|
|
1100
1177
|
|
|
1101
|
-
def _optional_int(value:
|
|
1178
|
+
def _optional_int(value: "object") -> "typing.Union[int, None]":
|
|
1102
1179
|
if value is None:
|
|
1103
1180
|
return None
|
|
1104
1181
|
return int(value)
|
|
1105
1182
|
|
|
1106
1183
|
|
|
1107
|
-
def _is_context_length_error_message(message:
|
|
1184
|
+
def _is_context_length_error_message(message: "str") -> "bool":
|
|
1108
1185
|
lower = message.lower()
|
|
1109
1186
|
return (
|
|
1110
1187
|
"context_length_exceeded" in lower
|
|
@@ -1114,7 +1191,7 @@ def _is_context_length_error_message(message: 'str') -> 'bool':
|
|
|
1114
1191
|
)
|
|
1115
1192
|
|
|
1116
1193
|
|
|
1117
|
-
def _requests_verify_setting() ->
|
|
1194
|
+
def _requests_verify_setting() -> "typing.Union[typing.Union[str, bool], None]":
|
|
1118
1195
|
for env_name in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE"):
|
|
1119
1196
|
value = os.environ.get(env_name, "").strip()
|
|
1120
1197
|
if value:
|