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.
Files changed (84) hide show
  1. pycodex/__init__.py +18 -14
  2. pycodex/agent.py +468 -462
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -436
  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 +329 -252
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +90 -52
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +235 -803
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -174
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +4 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +201 -0
  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 +13 -13
  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 +50 -66
  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/utils/image_utils.py +76 -0
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +263 -161
  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 +25 -22
  61. responses_server/messages_api.py +96 -49
  62. responses_server/payload_processors.py +25 -19
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +196 -107
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +51 -13
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +470 -384
  71. workspace_server/workspace.html +859 -232
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +168 -100
  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 -553
  80. python_codex-0.2.6.dist-info/METADATA +0 -441
  81. python_codex-0.2.6.dist-info/RECORD +0 -91
  82. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {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
- 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,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: '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
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: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
77
- profile: 'typing.Union[str, None]' = None,
78
- ) -> 'ResponsesProviderConfig':
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: 'typing.List[str]' = []
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(provider.get("stream_idle_timeout_ms")),
146
+ stream_idle_timeout_ms=_optional_int(
147
+ provider.get("stream_idle_timeout_ms")
148
+ ),
128
149
  )
129
150
 
130
- def api_key(self) -> 'typing.Union[str, None]':
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: 'typing.Union[str, None]' = None,
143
- reasoning_effort: 'typing.Union[str, None]' = None,
144
- ) -> 'ResponsesProviderConfig':
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) -> 'int':
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) -> 'float':
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) -> 'typing.Union[JSONDict, None]':
184
+ def metadata(self) -> "typing.Union[JSONDict, None]":
166
185
  return model_metadata(self.model)
167
186
 
168
- def use_responses_lite(self) -> 'bool':
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) -> 'typing.Union[str, None]':
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) -> 'typing.Union[str, None]':
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) -> 'typing.Union[str, None]':
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) -> 'typing.Union[str, None]':
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(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]":
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: 'typing.Union[JSONDict, None]',
240
- ) -> 'bool':
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: 'typing.Union[JSONDict, None]',
246
- key: 'str',
247
- ) -> 'typing.Union[str, None]':
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: 'typing.Iterable[object]') -> 'None':
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: 'typing.Iterable[object]') -> 'None':
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: 'str',
283
- partial_items: 'typing.Sequence[object]',
284
- reason: 'str' = "",
285
- ) -> 'None':
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: 'str',
295
- retry_delay_seconds: 'typing.Union[float, None]' = None,
296
- ) -> 'None':
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: 'int' = 0
304
- sse_events_received: 'int' = 0
305
- output_items_received: 'int' = 0
306
- last_sse_event_name: 'str' = ""
307
- last_event_type: 'str' = ""
308
- 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" = ""
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: 'ResponsesProviderConfig',
322
- timeout_seconds: 'float' = 120.0,
323
- session_id: 'typing.Union[str, None]' = None,
324
- originator: 'str' = DEFAULT_ORIGINATOR,
325
- user_agent: 'typing.Union[str, None]' = None,
326
- openai_subagent: 'typing.Union[str, None]' = None,
327
- ) -> '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":
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: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
340
- profile: 'typing.Union[str, None]' = None,
341
- timeout_seconds: 'float' = 120.0,
342
- originator: 'str' = DEFAULT_ORIGINATOR,
343
- user_agent: 'typing.Union[str, None]' = None,
344
- ) -> '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":
345
401
  config = ResponsesProviderConfig.from_codex_config(config_path, profile)
346
- 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
+ )
347
405
 
348
406
  def with_overrides(
349
407
  self,
350
- model: 'typing.Union[str, None]' = None,
351
- reasoning_effort: 'typing.Union[str, None]' = None,
352
- session_id: 'typing.Union[str, None]' = None,
353
- openai_subagent: 'typing.Union[str, None]' = None,
354
- ) -> '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":
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) -> 'str':
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) -> 'str':
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) -> 'typing.List[str]':
386
- return await asyncio.to_thread(self._list_models_sync)
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: 'Prompt',
391
- event_handler: 'ModelStreamEventHandler' = NOOP_MODEL_STREAM_EVENT_HANDLER,
392
- ) -> 'ModelResponse':
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
- while True:
396
- try:
397
- return await asyncio.to_thread(
398
- self._complete_sync,
399
- prompt,
400
- event_handler,
401
- )
402
- except (ResponsesRetryableError, ResponsesIncompleteError) as exc:
403
- if (
404
- isinstance(exc, ResponsesRetryableError)
405
- and _is_context_length_error_message(str(exc))
406
- ):
407
- raise ResponsesApiError(str(exc)) from exc
408
- if retries >= max_retries:
409
- raise
410
- retries += 1
411
- delay_seconds = (
412
- exc.retry_delay_seconds
413
- if isinstance(exc, ResponsesRetryableError)
414
- else None
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
- if delay_seconds > 0:
431
- 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
432
496
 
433
497
  def _complete_sync(
434
498
  self,
435
- prompt: 'Prompt',
436
- event_handler: 'ModelStreamEventHandler',
437
- ) -> 'ModelResponse':
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: 'Prompt') -> 'typing.Dict[str, object]':
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: 'typing.Dict[str, object]' = {
568
+ payload: "typing.Dict[str, object]" = {
503
569
  "model": self.model,
504
570
  "input": input_items,
505
- "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,
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
- 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]]" = [
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: 'typing.Dict[str, str]' = {}
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) -> 'typing.List[str]':
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: 'typing.List[str]' = []
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: 'Prompt') -> 'typing.Dict[str, str]':
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) -> 'typing.Dict[str, str]':
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: 'ModelStreamEventHandler',
656
- diagnostics: 'typing.Union[_StreamDiagnostics, None]' = None,
657
- ) -> 'ModelResponse':
658
- 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]]" = ([])
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
- event_payload = {
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
- event_payload["action_type"] = str(
698
- action_payload.get("type", "")
699
- )
700
- if "query" in action_payload:
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
- ModelStreamEvent(
715
- kind="tool_call",
716
- 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
+ ),
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: 'typing.Dict[str, object]',
782
- ) -> '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]":
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
- content = item.get("content", [])
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 = str(item.get("arguments", "") or "{}")
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: 'typing.Union[_StreamDiagnostics, None]' = None,
893
+ diagnostics: "typing.Union[_StreamDiagnostics, None]" = None,
822
894
  ):
823
- event_name: 'typing.Union[str, None]' = None
824
- data_lines: 'typing.List[str]' = []
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: '_StreamDiagnostics',
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: 'str',
876
- ) -> 'typing.List[typing.Tuple[str, str]]':
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: 'str',
887
- details: 'typing.Iterable[typing.Tuple[str, str]]',
888
- ) -> 'str':
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: 'str',
900
- exc: 'BaseException',
901
- diagnostics: 'typing.Union[_StreamDiagnostics, None]' = None,
902
- ) -> 'str':
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: 'str',
938
- code: 'str' = "",
939
- ) -> 'str':
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: 'typing.Dict[str, object]',
959
- output_item_count: 'int',
960
- reason: 'typing.Union[str, None]' = None,
961
- ) -> 'str':
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: 'typing.Dict[str, object]') -> 'str':
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(self, payload: 'typing.Dict[str, object]') -> 'None':
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(message):
998
- 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
+ )
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: 'str',
1016
- output_item_count: 'int',
1017
- ) -> 'str':
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: 'str',
1043
- raw_data: 'str',
1044
- exc: 'json.JSONDecodeError',
1045
- ) -> 'str':
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: '_StreamDiagnostics',
1060
- ) -> 'typing.List[typing.Tuple[str, str]]':
1061
- 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]]" = [
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: 'str', limit: 'int') -> 'str':
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: 'int') -> 'float':
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
- ) # 200s max
1159
+ ) # 200s max
1083
1160
 
1084
1161
  def _try_parse_retry_after_seconds(
1085
1162
  self,
1086
- code: 'str',
1087
- message: 'str',
1088
- ) -> 'typing.Union[float, None]':
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: 'object') -> 'typing.Union[int, None]':
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: 'str') -> 'bool':
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() -> 'typing.Union[typing.Union[str, bool], None]':
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: