python-codex 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
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
- import typing
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[[ModelStreamEvent], None]
36
- NOOP_MODEL_STREAM_EVENT_HANDLER: 'ModelStreamEventHandler' = lambda _event: None
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: 'Prompt',
50
- event_handler: 'ModelStreamEventHandler' = NOOP_MODEL_STREAM_EVENT_HANDLER,
51
- ) -> 'ModelResponse':
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
- @dataclass(frozen=True, )
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: 'str'
58
- provider_name: 'str'
59
- base_url: 'str'
60
- api_key_env: 'typing.Union[str, None]'
61
- use_chat_completion: 'bool' = False
62
- wire_api: 'str' = "responses"
63
- query_params: 'typing.Dict[str, str]' = field(default_factory=dict)
64
- reasoning_effort: 'typing.Union[str, None]' = None
65
- reasoning_summary: 'typing.Union[str, None]' = None
66
- verbosity: 'typing.Union[str, None]' = None
67
- sandbox_mode: 'typing.Union[str, None]' = None
68
- beta_features_header: 'typing.Union[str, None]' = None
69
- stream_max_retries: 'typing.Union[int, None]' = None
70
- stream_idle_timeout_ms: 'typing.Union[int, None]' = None
71
- service_tier: 'typing.Union[str, None]' = None
72
- responses_lite_override: 'typing.Union[bool, None]' = None
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: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
78
- profile: 'typing.Union[str, None]' = None,
79
- ) -> 'ResponsesProviderConfig':
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: 'typing.List[str]' = []
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(provider.get("stream_idle_timeout_ms")),
146
+ stream_idle_timeout_ms=_optional_int(
147
+ provider.get("stream_idle_timeout_ms")
148
+ ),
129
149
  )
130
150
 
131
- def api_key(self) -> 'typing.Union[str, None]':
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: 'typing.Union[str, None]' = None,
144
- reasoning_effort: 'typing.Union[str, None]' = None,
145
- ) -> 'ResponsesProviderConfig':
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) -> 'int':
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) -> 'float':
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) -> 'typing.Union[JSONDict, None]':
184
+ def metadata(self) -> "typing.Union[JSONDict, None]":
167
185
  return model_metadata(self.model)
168
186
 
169
- def use_responses_lite(self) -> 'bool':
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) -> 'typing.Union[str, None]':
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) -> 'typing.Union[str, None]':
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) -> 'typing.Union[str, None]':
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) -> 'typing.Union[str, None]':
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(value: 'typing.Union[bool, str, int, None]') -> 'typing.Union[bool, None]':
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: 'typing.Union[JSONDict, None]',
243
- ) -> 'bool':
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: 'typing.Union[JSONDict, None]',
249
- key: 'str',
250
- ) -> 'typing.Union[str, None]':
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: 'typing.Iterable[object]') -> 'None':
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: 'typing.Iterable[object]') -> 'None':
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: 'str',
286
- partial_items: 'typing.Sequence[object]',
287
- reason: 'str' = "",
288
- ) -> 'None':
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: 'str',
298
- retry_delay_seconds: 'typing.Union[float, None]' = None,
299
- ) -> 'None':
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: 'int' = 0
307
- sse_events_received: 'int' = 0
308
- output_items_received: 'int' = 0
309
- last_sse_event_name: 'str' = ""
310
- last_event_type: 'str' = ""
311
- last_payload_excerpt: 'str' = ""
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: 'ResponsesProviderConfig',
325
- timeout_seconds: 'float' = 120.0,
326
- session_id: 'typing.Union[str, None]' = None,
327
- originator: 'str' = DEFAULT_ORIGINATOR,
328
- user_agent: 'typing.Union[str, None]' = None,
329
- openai_subagent: 'typing.Union[str, None]' = None,
330
- ) -> 'None':
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: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
343
- profile: 'typing.Union[str, None]' = None,
344
- timeout_seconds: 'float' = 120.0,
345
- originator: 'str' = DEFAULT_ORIGINATOR,
346
- user_agent: 'typing.Union[str, None]' = None,
347
- ) -> 'ResponsesModelClient':
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(config, timeout_seconds, originator=originator, user_agent=user_agent)
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: 'typing.Union[str, None]' = None,
354
- reasoning_effort: 'typing.Union[str, None]' = None,
355
- session_id: 'typing.Union[str, None]' = None,
356
- openai_subagent: 'typing.Union[str, None]' = None,
357
- ) -> 'ResponsesModelClient':
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) -> 'str':
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) -> 'str':
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) -> 'typing.List[str]':
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) -> 'typing.List[str]':
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: 'Prompt',
397
- event_handler: 'ModelStreamEventHandler' = NOOP_MODEL_STREAM_EVENT_HANDLER,
398
- ) -> 'ModelResponse':
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
- while True:
402
- try:
403
- return await asyncio.to_thread(
404
- self._complete_sync,
405
- prompt,
406
- event_handler,
407
- )
408
- except (ResponsesRetryableError, ResponsesIncompleteError) as exc:
409
- if (
410
- isinstance(exc, ResponsesRetryableError)
411
- and _is_context_length_error_message(str(exc))
412
- ):
413
- raise ResponsesApiError(str(exc)) from exc
414
- if retries >= max_retries:
415
- raise
416
- retries += 1
417
- delay_seconds = (
418
- exc.retry_delay_seconds
419
- if isinstance(exc, ResponsesRetryableError)
420
- else None
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
- if delay_seconds > 0:
437
- await asyncio.sleep(delay_seconds)
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: 'Prompt',
442
- event_handler: 'ModelStreamEventHandler',
443
- ) -> 'ModelResponse':
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: 'Prompt') -> 'typing.Dict[str, object]':
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: 'typing.Dict[str, object]' = {
568
+ payload: "typing.Dict[str, object]" = {
509
569
  "model": self.model,
510
570
  "input": input_items,
511
- "parallel_tool_calls": prompt.parallel_tool_calls and not use_responses_lite,
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
- prefix: 'typing.List[typing.Dict[str, object]]' = [
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: 'typing.Dict[str, str]' = {}
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) -> 'typing.List[str]':
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: 'typing.List[str]' = []
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: 'Prompt') -> 'typing.Dict[str, str]':
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) -> 'typing.Dict[str, str]':
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: 'ModelStreamEventHandler',
662
- diagnostics: 'typing.Union[_StreamDiagnostics, None]' = None,
663
- ) -> 'ModelResponse':
664
- items: 'typing.List[typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem]]' = []
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
- event_payload = {
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
- event_payload["action_type"] = str(
704
- action_payload.get("type", "")
705
- )
706
- if "query" in action_payload:
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
- ModelStreamEvent(
721
- kind="tool_call",
722
- payload=event_payload,
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: 'typing.Dict[str, object]',
788
- ) -> 'typing.Union[typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], None]':
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
- content = item.get("content", [])
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 = str(item.get("arguments", "") or "{}")
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: 'typing.Union[_StreamDiagnostics, None]' = None,
893
+ diagnostics: "typing.Union[_StreamDiagnostics, None]" = None,
828
894
  ):
829
- event_name: 'typing.Union[str, None]' = None
830
- data_lines: 'typing.List[str]' = []
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: '_StreamDiagnostics',
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: 'str',
882
- ) -> 'typing.List[typing.Tuple[str, str]]':
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: 'str',
893
- details: 'typing.Iterable[typing.Tuple[str, str]]',
894
- ) -> 'str':
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: 'str',
906
- exc: 'BaseException',
907
- diagnostics: 'typing.Union[_StreamDiagnostics, None]' = None,
908
- ) -> 'str':
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: 'str',
944
- code: 'str' = "",
945
- ) -> 'str':
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: 'typing.Dict[str, object]',
965
- output_item_count: 'int',
966
- reason: 'typing.Union[str, None]' = None,
967
- ) -> 'str':
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: 'typing.Dict[str, object]') -> 'str':
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(self, payload: 'typing.Dict[str, object]') -> 'None':
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(message):
1004
- raise ResponsesApiError(self._format_response_failed_error(message, code))
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: 'str',
1022
- output_item_count: 'int',
1023
- ) -> 'str':
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: 'str',
1049
- raw_data: 'str',
1050
- exc: 'json.JSONDecodeError',
1051
- ) -> 'str':
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: '_StreamDiagnostics',
1066
- ) -> 'typing.List[typing.Tuple[str, str]]':
1067
- details: 'typing.List[typing.Tuple[str, str]]' = [
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: 'str', limit: 'int') -> 'str':
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: 'int') -> 'float':
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
- ) # 200s max
1159
+ ) # 200s max
1089
1160
 
1090
1161
  def _try_parse_retry_after_seconds(
1091
1162
  self,
1092
- code: 'str',
1093
- message: 'str',
1094
- ) -> 'typing.Union[float, None]':
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: 'object') -> 'typing.Union[int, None]':
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: 'str') -> 'bool':
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() -> 'typing.Union[typing.Union[str, bool], None]':
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: