codex-backend-sdk 0.3.1__tar.gz → 0.3.3__tar.gz

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 (37) hide show
  1. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/PKG-INFO +40 -9
  2. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/README.md +39 -8
  3. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/__init__.py +13 -1
  4. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_client.py +54 -13
  5. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_models.py +72 -1
  6. codex_backend_sdk-0.3.3/codex_backend_sdk/_transport.py +71 -0
  7. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_utils.py +3 -0
  8. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/codex_client.py +12 -0
  9. codex_backend_sdk-0.3.1/codex_backend_sdk/resources/responses.py → codex_backend_sdk-0.3.3/codex_backend_sdk/resources/_responses_payloads.py +114 -176
  10. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/codex.py +11 -6
  11. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/models.py +3 -2
  12. codex_backend_sdk-0.3.3/codex_backend_sdk/resources/responses.py +224 -0
  13. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/docs/backend-api.md +11 -3
  14. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/pyproject.toml +1 -1
  15. codex_backend_sdk-0.3.3/tests/test_client_retry.py +97 -0
  16. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_codex_resources.py +12 -8
  17. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_responses_resource.py +129 -23
  18. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/.gitignore +0 -0
  19. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/LICENSE +0 -0
  20. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_streaming.py +0 -0
  21. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/oauth.py +0 -0
  22. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/pkce.py +0 -0
  23. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/__init__.py +0 -0
  24. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/files.py +0 -0
  25. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/openai_oauth.py +0 -0
  26. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/realtime.py +0 -0
  27. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/storage.py +0 -0
  28. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/examples/agent.py +0 -0
  29. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/conftest.py +0 -0
  30. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_basic.py +0 -0
  31. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_conversation.py +0 -0
  32. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_files_resource.py +0 -0
  33. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_openai_oauth_resources.py +0 -0
  34. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_realtime_resource.py +0 -0
  35. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_reasoning.py +0 -0
  36. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_structured_output.py +0 -0
  37. {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-backend-sdk
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -240,6 +240,30 @@ response = client.responses.create(
240
240
  )
241
241
  ```
242
242
 
243
+ For structured output, `client.responses.parse(...)` accepts a Pydantic model,
244
+ sends it as a strict JSON schema, and returns `ParsedResponse`:
245
+
246
+ ```python
247
+ from pydantic import BaseModel
248
+
249
+
250
+ class Person(BaseModel):
251
+ name: str
252
+ age: int
253
+
254
+
255
+ parsed = client.responses.parse(
256
+ model="gpt-5.4",
257
+ input="Extract: Ada is 37 years old.",
258
+ text_format=Person,
259
+ )
260
+ print(parsed.output_parsed.name)
261
+ ```
262
+
263
+ Collected responses expose convenience properties for common output items:
264
+ `response.output_text`, `response.reasoning_summary`, and
265
+ `response.tool_calls`.
266
+
243
267
  Unsupported official Responses parameters are rejected explicitly with
244
268
  `CodexBackendUnsupportedParameterError`, including `temperature`, `top_p`,
245
269
  `max_output_tokens`, `metadata`, `user`, `safety_identifier`, `truncation`,
@@ -270,10 +294,11 @@ as opaque backend state.
270
294
 
271
295
  `client.models.list()` and `client.models.retrieve(model)` mirror the official
272
296
  OpenAI models resource, while preserving Codex-specific metadata as extra
273
- Pydantic fields.
297
+ Pydantic fields. The returned page also exposes the backend `ETag` when present.
274
298
 
275
299
  ```python
276
300
  models = client.models.list()
301
+ print(models.etag)
277
302
  for model in models:
278
303
  print(
279
304
  model.id,
@@ -413,23 +438,29 @@ These methods return raw backend dictionaries because these payloads can contain
413
438
  personal account-specific fields and may change without notice.
414
439
 
415
440
  `client.codex.memories.trace_summarize(...)` exposes the Codex memory
416
- summarization endpoint used by the official client. It returns the raw backend
417
- dictionary:
441
+ summarization endpoint used by the official client. It accepts dictionaries or
442
+ `RawMemory` objects and returns a typed `MemorySummarizeResponse`:
418
443
 
419
444
  ```python
445
+ from codex_backend_sdk import RawMemory
446
+
420
447
  summary = client.codex.memories.trace_summarize(
421
448
  model="gpt-5.4",
422
449
  traces=[
423
- {
424
- "id": "trace_1",
425
- "metadata": {"source_path": "memory.jsonl"},
426
- "items": [{"type": "message", "content": "Remember this"}],
427
- }
450
+ RawMemory(
451
+ id="trace_1",
452
+ metadata={"source_path": "memory.jsonl"},
453
+ items=[{"type": "message", "content": "Remember this"}],
454
+ )
428
455
  ],
429
456
  reasoning={"effort": "low"},
430
457
  )
458
+ print(summary.output[0].memory_summary)
431
459
  ```
432
460
 
461
+ Transient HTTP failures (`429`, `5xx`, timeouts, and connection errors) are
462
+ retried by default. Configure this with `OpenAI(max_retries=..., retry_base_delay=...)`.
463
+
433
464
  ### File Uploads
434
465
 
435
466
  `client.files.upload(...)` follows the official Codex file flow for Apps/MCP
@@ -214,6 +214,30 @@ response = client.responses.create(
214
214
  )
215
215
  ```
216
216
 
217
+ For structured output, `client.responses.parse(...)` accepts a Pydantic model,
218
+ sends it as a strict JSON schema, and returns `ParsedResponse`:
219
+
220
+ ```python
221
+ from pydantic import BaseModel
222
+
223
+
224
+ class Person(BaseModel):
225
+ name: str
226
+ age: int
227
+
228
+
229
+ parsed = client.responses.parse(
230
+ model="gpt-5.4",
231
+ input="Extract: Ada is 37 years old.",
232
+ text_format=Person,
233
+ )
234
+ print(parsed.output_parsed.name)
235
+ ```
236
+
237
+ Collected responses expose convenience properties for common output items:
238
+ `response.output_text`, `response.reasoning_summary`, and
239
+ `response.tool_calls`.
240
+
217
241
  Unsupported official Responses parameters are rejected explicitly with
218
242
  `CodexBackendUnsupportedParameterError`, including `temperature`, `top_p`,
219
243
  `max_output_tokens`, `metadata`, `user`, `safety_identifier`, `truncation`,
@@ -244,10 +268,11 @@ as opaque backend state.
244
268
 
245
269
  `client.models.list()` and `client.models.retrieve(model)` mirror the official
246
270
  OpenAI models resource, while preserving Codex-specific metadata as extra
247
- Pydantic fields.
271
+ Pydantic fields. The returned page also exposes the backend `ETag` when present.
248
272
 
249
273
  ```python
250
274
  models = client.models.list()
275
+ print(models.etag)
251
276
  for model in models:
252
277
  print(
253
278
  model.id,
@@ -387,23 +412,29 @@ These methods return raw backend dictionaries because these payloads can contain
387
412
  personal account-specific fields and may change without notice.
388
413
 
389
414
  `client.codex.memories.trace_summarize(...)` exposes the Codex memory
390
- summarization endpoint used by the official client. It returns the raw backend
391
- dictionary:
415
+ summarization endpoint used by the official client. It accepts dictionaries or
416
+ `RawMemory` objects and returns a typed `MemorySummarizeResponse`:
392
417
 
393
418
  ```python
419
+ from codex_backend_sdk import RawMemory
420
+
394
421
  summary = client.codex.memories.trace_summarize(
395
422
  model="gpt-5.4",
396
423
  traces=[
397
- {
398
- "id": "trace_1",
399
- "metadata": {"source_path": "memory.jsonl"},
400
- "items": [{"type": "message", "content": "Remember this"}],
401
- }
424
+ RawMemory(
425
+ id="trace_1",
426
+ metadata={"source_path": "memory.jsonl"},
427
+ items=[{"type": "message", "content": "Remember this"}],
428
+ )
402
429
  ],
403
430
  reasoning={"effort": "low"},
404
431
  )
432
+ print(summary.output[0].memory_summary)
405
433
  ```
406
434
 
435
+ Transient HTTP failures (`429`, `5xx`, timeouts, and connection errors) are
436
+ retried by default. Configure this with `OpenAI(max_retries=..., retry_base_delay=...)`.
437
+
407
438
  ### File Uploads
408
439
 
409
440
  `client.files.upload(...)` follows the official Codex file flow for Apps/MCP
@@ -17,7 +17,7 @@ Quickstart:
17
17
  print(response.output_text)
18
18
  """
19
19
 
20
- __version__ = "0.3.1"
20
+ __version__ = "0.3.3"
21
21
 
22
22
  from .oauth import run_oauth_flow, refresh_access_token, obtain_api_key
23
23
  from .storage import load_tokens, save_tokens, TokenStore
@@ -33,8 +33,14 @@ from .codex_client import (
33
33
  Verbosity,
34
34
  CodexClient,
35
35
  OpenAI,
36
+ MemorySummarizeOutput,
37
+ MemorySummarizeResponse,
36
38
  Model,
39
+ RawMemory,
40
+ RawMemoryMetadata,
41
+ ParsedResponse,
37
42
  Response,
43
+ ResponseFormatJsonSchema,
38
44
  ResponseStreamEvent,
39
45
  ResponseUsage,
40
46
  RealtimeCallResponse,
@@ -54,8 +60,14 @@ __all__ = [
54
60
  "CreateEmbeddingResponse",
55
61
  "Embedding",
56
62
  "EmbeddingUsage",
63
+ "MemorySummarizeOutput",
64
+ "MemorySummarizeResponse",
57
65
  "Model",
66
+ "ParsedResponse",
67
+ "RawMemory",
68
+ "RawMemoryMetadata",
58
69
  "Response",
70
+ "ResponseFormatJsonSchema",
59
71
  "ResponseStreamEvent",
60
72
  "ResponseUsage",
61
73
  "RealtimeCallResponse",
@@ -7,6 +7,7 @@ from typing import Any, Optional
7
7
 
8
8
  import requests
9
9
 
10
+ from ._transport import request_with_retries
10
11
  from ._utils import _UNSET, _is_given
11
12
  from .storage import TokenStore, load_tokens, save_tokens, token_needs_refresh
12
13
 
@@ -32,6 +33,8 @@ class CodexClient:
32
33
  model: str = "gpt-5.4",
33
34
  instructions: Optional[str] = None,
34
35
  timeout: float = 120,
36
+ max_retries: int = 2,
37
+ retry_base_delay: float = 0.25,
35
38
  ) -> None:
36
39
  from .resources.codex import CodexResources
37
40
  from .resources.models import Models
@@ -42,6 +45,8 @@ class CodexClient:
42
45
 
43
46
  self._store = store
44
47
  self._timeout = timeout
48
+ self._max_retries = max_retries
49
+ self._retry_base_delay = retry_base_delay
45
50
  self._session = requests.Session()
46
51
  self._defaults = {
47
52
  "model": model,
@@ -121,14 +126,30 @@ class CodexClient:
121
126
  return False
122
127
 
123
128
  def _get(self, path: str, *, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
129
+ return self._get_raw(path, params=params).json()
130
+
131
+ def _get_raw(
132
+ self,
133
+ path: str,
134
+ *,
135
+ params: Optional[dict[str, Any]] = None,
136
+ headers: Optional[dict[str, str]] = None,
137
+ timeout: Any = _UNSET,
138
+ ) -> requests.Response:
124
139
  self._ensure_auth()
125
- response = self._session.get(f"{BASE_URL}{path}", params=params, timeout=self._timeout)
126
- response.raise_for_status()
127
- return response.json()
140
+ response = self._request_with_retries(
141
+ "GET",
142
+ f"{BASE_URL}{path}",
143
+ params=params,
144
+ headers=headers,
145
+ timeout=self._timeout if not _is_given(timeout) else timeout,
146
+ )
147
+ return response
128
148
 
129
149
  def _post(self, path: str, *, body: dict[str, Any], stream: bool = False) -> requests.Response:
130
150
  self._ensure_auth()
131
- response = self._session.post(
151
+ response = self._request_with_retries(
152
+ "POST",
132
153
  f"{BASE_URL}{path}",
133
154
  json=body,
134
155
  headers={"Accept": "text/event-stream"} if stream else None,
@@ -151,7 +172,8 @@ class CodexClient:
151
172
  timeout: Any = _UNSET,
152
173
  ) -> requests.Response:
153
174
  self._ensure_auth()
154
- response = self._session.post(
175
+ response = self._request_with_retries(
176
+ "POST",
155
177
  f"{BASE_URL}{path}",
156
178
  json=body if files is None and data is None and content is None else None,
157
179
  data=content if content is not None else data,
@@ -178,12 +200,14 @@ class CodexClient:
178
200
  params: Optional[dict[str, Any]] = None,
179
201
  timeout: Any = _UNSET,
180
202
  ) -> dict[str, Any]:
181
- response = requests.post(
203
+ response = self._request_with_retries(
204
+ "POST",
182
205
  f"{OPENAI_BASE_URL}{path}",
183
206
  json=body,
184
207
  headers=self._openai_headers(headers),
185
208
  params=params,
186
209
  timeout=self._timeout if not _is_given(timeout) else timeout,
210
+ _use_session=False,
187
211
  )
188
212
  response.raise_for_status()
189
213
  return response.json()
@@ -200,7 +224,8 @@ class CodexClient:
200
224
  timeout: Any = _UNSET,
201
225
  stream: bool = False,
202
226
  ) -> requests.Response:
203
- response = requests.post(
227
+ response = self._request_with_retries(
228
+ "POST",
204
229
  f"{OPENAI_BASE_URL}{path}",
205
230
  json=body if files is None and data is None else None,
206
231
  data=data,
@@ -209,20 +234,24 @@ class CodexClient:
209
234
  params=params,
210
235
  stream=stream,
211
236
  timeout=self._timeout if not _is_given(timeout) else timeout,
237
+ _use_session=False,
212
238
  )
213
239
  response.raise_for_status()
214
240
  return response
215
241
 
216
242
  def _get_wham(self, path: str, *, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
217
243
  self._ensure_auth()
218
- response = self._session.get(f"{WHAM_BASE_URL}{path}", params=params, timeout=30)
219
- response.raise_for_status()
244
+ response = self._request_with_retries(
245
+ "GET",
246
+ f"{WHAM_BASE_URL}{path}",
247
+ params=params,
248
+ timeout=30,
249
+ )
220
250
  return response.json()
221
251
 
222
252
  def _get_chatgpt(self, path: str) -> dict[str, Any]:
223
253
  self._ensure_auth()
224
- response = self._session.get(f"{CHATGPT_BASE_URL}{path}", timeout=30)
225
- response.raise_for_status()
254
+ response = self._request_with_retries("GET", f"{CHATGPT_BASE_URL}{path}", timeout=30)
226
255
  return response.json()
227
256
 
228
257
  def _post_chatgpt(
@@ -233,14 +262,26 @@ class CodexClient:
233
262
  timeout: Any = _UNSET,
234
263
  ) -> dict[str, Any]:
235
264
  self._ensure_auth()
236
- response = self._session.post(
265
+ response = self._request_with_retries(
266
+ "POST",
237
267
  f"{CHATGPT_BASE_URL}{path}",
238
268
  json=body,
239
269
  timeout=self._timeout if not _is_given(timeout) else timeout,
240
270
  )
241
- response.raise_for_status()
242
271
  return response.json()
243
272
 
273
+ def _request_with_retries(self, method: str, url: str, **kwargs: Any) -> requests.Response:
274
+ use_session = kwargs.pop("_use_session", True)
275
+ return request_with_retries(
276
+ self._session,
277
+ method,
278
+ url,
279
+ max_retries=self._max_retries,
280
+ retry_base_delay=self._retry_base_delay,
281
+ use_session=use_session,
282
+ **kwargs,
283
+ )
284
+
244
285
  def realtime_websocket_url(self, *, model: str) -> str:
245
286
  """Return the official OpenAI Realtime WebSocket URL for Codex plugins."""
246
287
  if not model:
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import time
6
6
  from collections.abc import Iterator
7
- from typing import Any, Literal, Optional
7
+ from typing import Any, Generic, Literal, Optional, TypeVar
8
8
 
9
9
  import requests
10
10
  from pydantic import BaseModel, ConfigDict, Field
@@ -13,6 +13,7 @@ ReasoningEffort = Literal["minimal", "low", "medium", "high", "xhigh"]
13
13
  ReasoningSummary = Literal["concise", "detailed", "auto"]
14
14
  Verbosity = Literal["low", "medium", "high"]
15
15
  ServiceTier = Literal["flex", "priority"]
16
+ ParsedT = TypeVar("ParsedT")
16
17
 
17
18
 
18
19
  class CodexBaseModel(BaseModel):
@@ -73,6 +74,14 @@ class ResponseUsage(CodexBaseModel):
73
74
  output_tokens_details: TokenDetails = Field(default_factory=TokenDetails)
74
75
 
75
76
 
77
+ class ResponseFormatJsonSchema(CodexBaseModel):
78
+ type: Literal["json_schema"] = "json_schema"
79
+ name: str
80
+ schema_: dict[str, Any] = Field(alias="schema")
81
+ strict: Optional[bool] = None
82
+ description: Optional[str] = None
83
+
84
+
76
85
  class Response(CodexBaseModel):
77
86
  id: str
78
87
  created_at: float = Field(default_factory=time.time)
@@ -117,6 +126,49 @@ class Response(CodexBaseModel):
117
126
  texts.append(content.get("text", ""))
118
127
  return "".join(texts)
119
128
 
129
+ @property
130
+ def reasoning_summary(self) -> str | None:
131
+ texts: list[str] = []
132
+ for output in self.output:
133
+ if output.get("type") == "reasoning":
134
+ for summary in output.get("summary", []):
135
+ if isinstance(summary, dict):
136
+ texts.append(summary.get("text", ""))
137
+ return "\n".join(text for text in texts if text) or None
138
+
139
+ @property
140
+ def tool_calls(self) -> list[dict[str, Any]]:
141
+ return [output for output in self.output if output.get("type") == "function_call"]
142
+
143
+
144
+ class ParsedResponse(CodexBaseModel, Generic[ParsedT]):
145
+ response: Response
146
+ output_parsed: ParsedT
147
+
148
+ @property
149
+ def id(self) -> str:
150
+ return self.response.id
151
+
152
+ @property
153
+ def model(self) -> Optional[str]:
154
+ return self.response.model
155
+
156
+ @property
157
+ def output(self) -> list[dict[str, Any]]:
158
+ return self.response.output
159
+
160
+ @property
161
+ def output_text(self) -> str:
162
+ return self.response.output_text
163
+
164
+ @property
165
+ def status(self) -> Optional[str]:
166
+ return self.response.status
167
+
168
+ @property
169
+ def usage(self) -> Optional[ResponseUsage]:
170
+ return self.response.usage
171
+
120
172
 
121
173
  class ResponseStreamEvent(CodexBaseModel):
122
174
  type: str
@@ -173,6 +225,25 @@ class Transcription(CodexBaseModel):
173
225
  text: str = ""
174
226
 
175
227
 
228
+ class RawMemoryMetadata(CodexBaseModel):
229
+ source_path: str
230
+
231
+
232
+ class RawMemory(CodexBaseModel):
233
+ id: str
234
+ metadata: RawMemoryMetadata
235
+ items: list[Any] = Field(default_factory=list)
236
+
237
+
238
+ class MemorySummarizeOutput(CodexBaseModel):
239
+ raw_memory: str = Field(default="", alias="trace_summary")
240
+ memory_summary: str = ""
241
+
242
+
243
+ class MemorySummarizeResponse(CodexBaseModel):
244
+ output: list[MemorySummarizeOutput] = Field(default_factory=list)
245
+
246
+
176
247
  class UploadedFile(CodexBaseModel):
177
248
  file_id: str
178
249
  uri: str
@@ -0,0 +1,71 @@
1
+ """HTTP transport helpers shared by client resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ import requests
9
+
10
+
11
+ def request_with_retries(
12
+ session: requests.Session,
13
+ method: str,
14
+ url: str,
15
+ *,
16
+ max_retries: int,
17
+ retry_base_delay: float,
18
+ use_session: bool = True,
19
+ **kwargs: Any,
20
+ ) -> requests.Response:
21
+ last_error: requests.RequestException | None = None
22
+ for attempt in range(max_retries + 1):
23
+ try:
24
+ request = session.request if use_session else requests.request
25
+ response = request(method, url, **kwargs)
26
+ if should_retry_response(response, attempt, max_retries=max_retries):
27
+ sleep_before_retry(response, attempt, retry_base_delay=retry_base_delay)
28
+ continue
29
+ response.raise_for_status()
30
+ return response
31
+ except (requests.Timeout, requests.ConnectionError) as exc:
32
+ last_error = exc
33
+ if attempt >= max_retries:
34
+ raise
35
+ sleep_before_retry(None, attempt, retry_base_delay=retry_base_delay)
36
+ if last_error is not None:
37
+ raise last_error
38
+ raise RuntimeError("Request retry loop exhausted")
39
+
40
+
41
+ def should_retry_response(
42
+ response: requests.Response,
43
+ attempt: int,
44
+ *,
45
+ max_retries: int,
46
+ ) -> bool:
47
+ if attempt >= max_retries:
48
+ return False
49
+ return response.status_code == 429 or 500 <= response.status_code < 600
50
+
51
+
52
+ def sleep_before_retry(
53
+ response: requests.Response | None,
54
+ attempt: int,
55
+ *,
56
+ retry_base_delay: float,
57
+ ) -> None:
58
+ retry_after = response.headers.get("Retry-After") if response is not None else None
59
+ delay = parse_retry_after(retry_after)
60
+ if delay is None:
61
+ delay = retry_base_delay * (2 ** attempt)
62
+ time.sleep(delay)
63
+
64
+
65
+ def parse_retry_after(value: str | None) -> float | None:
66
+ if not value:
67
+ return None
68
+ try:
69
+ return max(0.0, float(value))
70
+ except ValueError:
71
+ return None
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import base64
6
6
  import json
7
+ from dataclasses import asdict, is_dataclass
7
8
  from typing import Any
8
9
 
9
10
  from pydantic import BaseModel
@@ -26,6 +27,8 @@ def image_b64(data: str, media_type: str = "image/jpeg") -> dict[str, str]:
26
27
  def _jsonable(value: Any) -> Any:
27
28
  if isinstance(value, BaseModel):
28
29
  return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
30
+ if is_dataclass(value) and not isinstance(value, type):
31
+ return _jsonable(asdict(value))
29
32
  if isinstance(value, dict):
30
33
  return {key: _jsonable(item) for key, item in value.items()}
31
34
  if isinstance(value, list):
@@ -14,10 +14,16 @@ from ._models import (
14
14
  Embedding,
15
15
  EmbeddingUsage,
16
16
  Model,
17
+ MemorySummarizeOutput,
18
+ MemorySummarizeResponse,
19
+ ParsedResponse,
20
+ RawMemory,
21
+ RawMemoryMetadata,
17
22
  ReasoningEffort,
18
23
  ReasoningSummary,
19
24
  RealtimeCallResponse,
20
25
  Response,
26
+ ResponseFormatJsonSchema,
21
27
  ResponseStreamEvent,
22
28
  ResponseUsage,
23
29
  ServiceTier,
@@ -37,11 +43,17 @@ __all__ = [
37
43
  "Embedding",
38
44
  "EmbeddingUsage",
39
45
  "CompactedResponse",
46
+ "MemorySummarizeOutput",
47
+ "MemorySummarizeResponse",
40
48
  "Model",
41
49
  "OpenAI",
50
+ "ParsedResponse",
51
+ "RawMemory",
52
+ "RawMemoryMetadata",
42
53
  "ReasoningEffort",
43
54
  "ReasoningSummary",
44
55
  "Response",
56
+ "ResponseFormatJsonSchema",
45
57
  "ResponseStreamEvent",
46
58
  "ResponseUsage",
47
59
  "RealtimeCallResponse",