codex-backend-sdk 0.3.0__tar.gz → 0.3.2__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 (36) hide show
  1. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/PKG-INFO +43 -5
  2. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/README.md +42 -4
  3. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/__init__.py +11 -1
  4. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_client.py +103 -12
  5. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_models.py +29 -0
  6. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/codex_client.py +10 -0
  7. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/codex.py +30 -0
  8. codex_backend_sdk-0.3.2/codex_backend_sdk/resources/files.py +116 -0
  9. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/models.py +19 -4
  10. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/realtime.py +5 -11
  11. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/responses.py +19 -2
  12. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/docs/backend-api.md +57 -3
  13. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/pyproject.toml +1 -1
  14. codex_backend_sdk-0.3.2/tests/test_client_retry.py +97 -0
  15. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_codex_resources.py +66 -1
  16. codex_backend_sdk-0.3.2/tests/test_files_resource.py +114 -0
  17. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_realtime_resource.py +6 -4
  18. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_responses_resource.py +89 -16
  19. codex_backend_sdk-0.3.0/tools/probe_v1_endpoints.py +0 -269
  20. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/.gitignore +0 -0
  21. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/LICENSE +0 -0
  22. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_streaming.py +0 -0
  23. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_utils.py +0 -0
  24. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/oauth.py +0 -0
  25. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/pkce.py +0 -0
  26. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/__init__.py +0 -0
  27. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/openai_oauth.py +0 -0
  28. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/codex_backend_sdk/storage.py +0 -0
  29. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/examples/agent.py +0 -0
  30. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/conftest.py +0 -0
  31. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_basic.py +0 -0
  32. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_conversation.py +0 -0
  33. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_openai_oauth_resources.py +0 -0
  34. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_reasoning.py +0 -0
  35. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/tests/test_structured_output.py +0 -0
  36. {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.2}/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.0
3
+ Version: 0.3.2
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -184,17 +184,20 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
184
184
  |---|---|---|
185
185
  | `POST /backend-api/codex/responses` | `client.responses.create(...)` | Stream-only backend; non-streaming SDK calls are collected from SSE events. |
186
186
  | `POST /backend-api/codex/responses/compact` | `client.responses.compact(...)` | Codex-specific helper for encrypted context compaction. |
187
+ | `POST /backend-api/codex/memories/trace_summarize` | `client.codex.memories.trace_summarize(...)` | Raw Codex memory trace summarization helper. |
187
188
  | `GET /backend-api/codex/models` | `client.models.list()` / `client.models.retrieve(...)` | OpenAI-shaped model objects with Codex metadata preserved as extra fields. |
188
189
  | `POST /backend-api/codex/realtime/calls` | `client.realtime.calls.create(...)` | OpenAI-shaped SDP call creation for realtime sessions. |
189
190
  | `wss://api.openai.com/v1/realtime?model=...` | `client.realtime_websocket_url(...)` / `client.realtime_websocket_headers(...)` | Helper surface used by codex-agent's realtime plugin. |
190
191
  | `POST /v1/embeddings` | `client.embeddings.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; verified with `text-embedding-3-small`. |
191
192
  | `POST /v1/audio/transcriptions` | `client.audio.transcriptions.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; verified with `gpt-4o-mini-transcribe`. |
192
193
  | `GET /backend-api/wham/usage` | `client.codex.usage()` | Codex/ChatGPT quota and rate-limit status. |
194
+ | `GET /backend-api/wham/config/requirements` | `client.codex.config.requirements()` | Raw managed requirements/config payload for the authenticated account. |
193
195
  | `GET /backend-api/wham/tasks/list` | `client.codex.tasks.list(...)` | Raw Codex cloud task listing. |
194
196
  | `GET /backend-api/wham/tasks/{task_id}` | `client.codex.tasks.retrieve(task_id)` | Raw Codex cloud task detail. |
195
197
  | `GET /backend-api/wham/tasks/{task_id}/turns` | `client.codex.tasks.turns.list(task_id)` | Raw task turn mapping. |
196
198
  | `GET /backend-api/wham/tasks/{task_id}/turns/{turn_id}/sibling_turns` | `client.codex.tasks.turns.sibling_turns(task_id, turn_id)` | Raw sibling turn list. |
197
199
  | `GET /backend-api/wham/environments` | `client.codex.environments.list()` | Raw Codex cloud environment list. |
200
+ | `POST /backend-api/files` + signed upload | `client.files.upload(...)` | Uploads local files for Codex Apps/MCP file parameters and returns `sediment://...` metadata. |
198
201
  | `GET /backend-api/memories` | `client.codex.memories.list()` | Raw ChatGPT memory payload for the authenticated account. |
199
202
  | `GET /backend-api/user_system_messages` | `client.codex.user_system_messages.retrieve()` | Raw ChatGPT customization/system-message payload. |
200
203
 
@@ -267,10 +270,11 @@ as opaque backend state.
267
270
 
268
271
  `client.models.list()` and `client.models.retrieve(model)` mirror the official
269
272
  OpenAI models resource, while preserving Codex-specific metadata as extra
270
- Pydantic fields.
273
+ Pydantic fields. The returned page also exposes the backend `ETag` when present.
271
274
 
272
275
  ```python
273
276
  models = client.models.list()
277
+ print(models.etag)
274
278
  for model in models:
275
279
  print(
276
280
  model.id,
@@ -403,18 +407,52 @@ not part of the official OpenAI SDK.
403
407
  ```python
404
408
  memories = client.codex.memories.list()
405
409
  customization = client.codex.user_system_messages.retrieve()
410
+ requirements = client.codex.config.requirements()
406
411
  ```
407
412
 
408
- Both methods return raw backend dictionaries because these payloads can contain
413
+ These methods return raw backend dictionaries because these payloads can contain
409
414
  personal account-specific fields and may change without notice.
410
415
 
416
+ `client.codex.memories.trace_summarize(...)` exposes the Codex memory
417
+ summarization endpoint used by the official client. It accepts dictionaries or
418
+ `RawMemory` objects and returns a typed `MemorySummarizeResponse`:
419
+
420
+ ```python
421
+ from codex_backend_sdk import RawMemory
422
+
423
+ summary = client.codex.memories.trace_summarize(
424
+ model="gpt-5.4",
425
+ traces=[
426
+ RawMemory(
427
+ id="trace_1",
428
+ metadata={"source_path": "memory.jsonl"},
429
+ items=[{"type": "message", "content": "Remember this"}],
430
+ )
431
+ ],
432
+ reasoning={"effort": "low"},
433
+ )
434
+ print(summary.output[0].memory_summary)
435
+ ```
436
+
437
+ Transient HTTP failures (`429`, `5xx`, timeouts, and connection errors) are
438
+ retried by default. Configure this with `OpenAI(max_retries=..., retry_base_delay=...)`.
439
+
440
+ ### File Uploads
441
+
442
+ `client.files.upload(...)` follows the official Codex file flow for Apps/MCP
443
+ file parameters: create file metadata under ChatGPT, upload bytes to the signed
444
+ URL, then finalize the upload.
445
+
446
+ ```python
447
+ uploaded = client.files.upload("report.csv")
448
+ print(uploaded.uri) # sediment://file_...
449
+ ```
450
+
411
451
  ### Observed But Not Exposed
412
452
 
413
453
  The reverse-engineering notes in `docs/backend-api.md` include additional
414
454
  observed endpoints. They are not exposed as SDK resources yet because they are
415
455
  plan-gated, unavailable on `chatgpt.com`, or not stable enough:
416
456
 
417
- - `POST /backend-api/codex/memories/trace_summarize`
418
- - `GET /backend-api/wham/config/requirements`
419
457
  - `POST /v1/audio/speech` (auth reaches the endpoint, but Pro OAuth lacks
420
458
  `api.model.audio.request` in current tests)
@@ -158,17 +158,20 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
158
158
  |---|---|---|
159
159
  | `POST /backend-api/codex/responses` | `client.responses.create(...)` | Stream-only backend; non-streaming SDK calls are collected from SSE events. |
160
160
  | `POST /backend-api/codex/responses/compact` | `client.responses.compact(...)` | Codex-specific helper for encrypted context compaction. |
161
+ | `POST /backend-api/codex/memories/trace_summarize` | `client.codex.memories.trace_summarize(...)` | Raw Codex memory trace summarization helper. |
161
162
  | `GET /backend-api/codex/models` | `client.models.list()` / `client.models.retrieve(...)` | OpenAI-shaped model objects with Codex metadata preserved as extra fields. |
162
163
  | `POST /backend-api/codex/realtime/calls` | `client.realtime.calls.create(...)` | OpenAI-shaped SDP call creation for realtime sessions. |
163
164
  | `wss://api.openai.com/v1/realtime?model=...` | `client.realtime_websocket_url(...)` / `client.realtime_websocket_headers(...)` | Helper surface used by codex-agent's realtime plugin. |
164
165
  | `POST /v1/embeddings` | `client.embeddings.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; verified with `text-embedding-3-small`. |
165
166
  | `POST /v1/audio/transcriptions` | `client.audio.transcriptions.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; verified with `gpt-4o-mini-transcribe`. |
166
167
  | `GET /backend-api/wham/usage` | `client.codex.usage()` | Codex/ChatGPT quota and rate-limit status. |
168
+ | `GET /backend-api/wham/config/requirements` | `client.codex.config.requirements()` | Raw managed requirements/config payload for the authenticated account. |
167
169
  | `GET /backend-api/wham/tasks/list` | `client.codex.tasks.list(...)` | Raw Codex cloud task listing. |
168
170
  | `GET /backend-api/wham/tasks/{task_id}` | `client.codex.tasks.retrieve(task_id)` | Raw Codex cloud task detail. |
169
171
  | `GET /backend-api/wham/tasks/{task_id}/turns` | `client.codex.tasks.turns.list(task_id)` | Raw task turn mapping. |
170
172
  | `GET /backend-api/wham/tasks/{task_id}/turns/{turn_id}/sibling_turns` | `client.codex.tasks.turns.sibling_turns(task_id, turn_id)` | Raw sibling turn list. |
171
173
  | `GET /backend-api/wham/environments` | `client.codex.environments.list()` | Raw Codex cloud environment list. |
174
+ | `POST /backend-api/files` + signed upload | `client.files.upload(...)` | Uploads local files for Codex Apps/MCP file parameters and returns `sediment://...` metadata. |
172
175
  | `GET /backend-api/memories` | `client.codex.memories.list()` | Raw ChatGPT memory payload for the authenticated account. |
173
176
  | `GET /backend-api/user_system_messages` | `client.codex.user_system_messages.retrieve()` | Raw ChatGPT customization/system-message payload. |
174
177
 
@@ -241,10 +244,11 @@ as opaque backend state.
241
244
 
242
245
  `client.models.list()` and `client.models.retrieve(model)` mirror the official
243
246
  OpenAI models resource, while preserving Codex-specific metadata as extra
244
- Pydantic fields.
247
+ Pydantic fields. The returned page also exposes the backend `ETag` when present.
245
248
 
246
249
  ```python
247
250
  models = client.models.list()
251
+ print(models.etag)
248
252
  for model in models:
249
253
  print(
250
254
  model.id,
@@ -377,18 +381,52 @@ not part of the official OpenAI SDK.
377
381
  ```python
378
382
  memories = client.codex.memories.list()
379
383
  customization = client.codex.user_system_messages.retrieve()
384
+ requirements = client.codex.config.requirements()
380
385
  ```
381
386
 
382
- Both methods return raw backend dictionaries because these payloads can contain
387
+ These methods return raw backend dictionaries because these payloads can contain
383
388
  personal account-specific fields and may change without notice.
384
389
 
390
+ `client.codex.memories.trace_summarize(...)` exposes the Codex memory
391
+ summarization endpoint used by the official client. It accepts dictionaries or
392
+ `RawMemory` objects and returns a typed `MemorySummarizeResponse`:
393
+
394
+ ```python
395
+ from codex_backend_sdk import RawMemory
396
+
397
+ summary = client.codex.memories.trace_summarize(
398
+ model="gpt-5.4",
399
+ traces=[
400
+ RawMemory(
401
+ id="trace_1",
402
+ metadata={"source_path": "memory.jsonl"},
403
+ items=[{"type": "message", "content": "Remember this"}],
404
+ )
405
+ ],
406
+ reasoning={"effort": "low"},
407
+ )
408
+ print(summary.output[0].memory_summary)
409
+ ```
410
+
411
+ Transient HTTP failures (`429`, `5xx`, timeouts, and connection errors) are
412
+ retried by default. Configure this with `OpenAI(max_retries=..., retry_base_delay=...)`.
413
+
414
+ ### File Uploads
415
+
416
+ `client.files.upload(...)` follows the official Codex file flow for Apps/MCP
417
+ file parameters: create file metadata under ChatGPT, upload bytes to the signed
418
+ URL, then finalize the upload.
419
+
420
+ ```python
421
+ uploaded = client.files.upload("report.csv")
422
+ print(uploaded.uri) # sediment://file_...
423
+ ```
424
+
385
425
  ### Observed But Not Exposed
386
426
 
387
427
  The reverse-engineering notes in `docs/backend-api.md` include additional
388
428
  observed endpoints. They are not exposed as SDK resources yet because they are
389
429
  plan-gated, unavailable on `chatgpt.com`, or not stable enough:
390
430
 
391
- - `POST /backend-api/codex/memories/trace_summarize`
392
- - `GET /backend-api/wham/config/requirements`
393
431
  - `POST /v1/audio/speech` (auth reaches the endpoint, but Pro OAuth lacks
394
432
  `api.model.audio.request` in current tests)
@@ -17,7 +17,7 @@ Quickstart:
17
17
  print(response.output_text)
18
18
  """
19
19
 
20
- __version__ = "0.3.0"
20
+ __version__ = "0.3.2"
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,7 +33,11 @@ from .codex_client import (
33
33
  Verbosity,
34
34
  CodexClient,
35
35
  OpenAI,
36
+ MemorySummarizeOutput,
37
+ MemorySummarizeResponse,
36
38
  Model,
39
+ RawMemory,
40
+ RawMemoryMetadata,
37
41
  Response,
38
42
  ResponseStreamEvent,
39
43
  ResponseUsage,
@@ -41,6 +45,7 @@ from .codex_client import (
41
45
  SyncPage,
42
46
  CompactedResponse,
43
47
  Transcription,
48
+ UploadedFile,
44
49
  image_url,
45
50
  image_b64,
46
51
  )
@@ -53,7 +58,11 @@ __all__ = [
53
58
  "CreateEmbeddingResponse",
54
59
  "Embedding",
55
60
  "EmbeddingUsage",
61
+ "MemorySummarizeOutput",
62
+ "MemorySummarizeResponse",
56
63
  "Model",
64
+ "RawMemory",
65
+ "RawMemoryMetadata",
57
66
  "Response",
58
67
  "ResponseStreamEvent",
59
68
  "ResponseUsage",
@@ -61,6 +70,7 @@ __all__ = [
61
70
  "SyncPage",
62
71
  "CompactedResponse",
63
72
  "Transcription",
73
+ "UploadedFile",
64
74
  "ReasoningEffort",
65
75
  "ReasoningSummary",
66
76
  "ServiceTier",
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import urllib.parse
6
+ import time
6
7
  from typing import Any, Optional
7
8
 
8
9
  import requests
@@ -32,15 +33,20 @@ 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
38
41
  from .resources.openai_oauth import Audio, Embeddings
42
+ from .resources.files import Files
39
43
  from .resources.realtime import Realtime
40
44
  from .resources.responses import Responses
41
45
 
42
46
  self._store = store
43
47
  self._timeout = timeout
48
+ self._max_retries = max_retries
49
+ self._retry_base_delay = retry_base_delay
44
50
  self._session = requests.Session()
45
51
  self._defaults = {
46
52
  "model": model,
@@ -53,6 +59,7 @@ class CodexClient:
53
59
  self.realtime = Realtime(self)
54
60
  self.embeddings = Embeddings(self)
55
61
  self.audio = Audio(self)
62
+ self.files = Files(self)
56
63
  self.codex = CodexResources(self)
57
64
 
58
65
  def authenticate(self, *, request_api_key: bool = True) -> "CodexClient":
@@ -119,14 +126,30 @@ class CodexClient:
119
126
  return False
120
127
 
121
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:
122
139
  self._ensure_auth()
123
- response = self._session.get(f"{BASE_URL}{path}", params=params, timeout=self._timeout)
124
- response.raise_for_status()
125
- 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
126
148
 
127
149
  def _post(self, path: str, *, body: dict[str, Any], stream: bool = False) -> requests.Response:
128
150
  self._ensure_auth()
129
- response = self._session.post(
151
+ response = self._request_with_retries(
152
+ "POST",
130
153
  f"{BASE_URL}{path}",
131
154
  json=body,
132
155
  headers={"Accept": "text/event-stream"} if stream else None,
@@ -140,6 +163,7 @@ class CodexClient:
140
163
  self,
141
164
  path: str,
142
165
  *,
166
+ body: Optional[dict[str, Any]] = None,
143
167
  content: Optional[bytes] = None,
144
168
  files: Any = None,
145
169
  data: Any = None,
@@ -148,9 +172,11 @@ class CodexClient:
148
172
  timeout: Any = _UNSET,
149
173
  ) -> requests.Response:
150
174
  self._ensure_auth()
151
- response = self._session.post(
175
+ response = self._request_with_retries(
176
+ "POST",
152
177
  f"{BASE_URL}{path}",
153
- data=content if files is None else data,
178
+ json=body if files is None and data is None and content is None else None,
179
+ data=content if content is not None else data,
154
180
  files=files,
155
181
  headers=headers,
156
182
  params=params,
@@ -174,12 +200,14 @@ class CodexClient:
174
200
  params: Optional[dict[str, Any]] = None,
175
201
  timeout: Any = _UNSET,
176
202
  ) -> dict[str, Any]:
177
- response = requests.post(
203
+ response = self._request_with_retries(
204
+ "POST",
178
205
  f"{OPENAI_BASE_URL}{path}",
179
206
  json=body,
180
207
  headers=self._openai_headers(headers),
181
208
  params=params,
182
209
  timeout=self._timeout if not _is_given(timeout) else timeout,
210
+ _use_session=False,
183
211
  )
184
212
  response.raise_for_status()
185
213
  return response.json()
@@ -196,7 +224,8 @@ class CodexClient:
196
224
  timeout: Any = _UNSET,
197
225
  stream: bool = False,
198
226
  ) -> requests.Response:
199
- response = requests.post(
227
+ response = self._request_with_retries(
228
+ "POST",
200
229
  f"{OPENAI_BASE_URL}{path}",
201
230
  json=body if files is None and data is None else None,
202
231
  data=data,
@@ -205,22 +234,75 @@ class CodexClient:
205
234
  params=params,
206
235
  stream=stream,
207
236
  timeout=self._timeout if not _is_given(timeout) else timeout,
237
+ _use_session=False,
208
238
  )
209
239
  response.raise_for_status()
210
240
  return response
211
241
 
212
242
  def _get_wham(self, path: str, *, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
213
243
  self._ensure_auth()
214
- response = self._session.get(f"{WHAM_BASE_URL}{path}", params=params, timeout=30)
215
- 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
+ )
216
250
  return response.json()
217
251
 
218
252
  def _get_chatgpt(self, path: str) -> dict[str, Any]:
219
253
  self._ensure_auth()
220
- response = self._session.get(f"{CHATGPT_BASE_URL}{path}", timeout=30)
221
- response.raise_for_status()
254
+ response = self._request_with_retries("GET", f"{CHATGPT_BASE_URL}{path}", timeout=30)
255
+ return response.json()
256
+
257
+ def _post_chatgpt(
258
+ self,
259
+ path: str,
260
+ *,
261
+ body: dict[str, Any],
262
+ timeout: Any = _UNSET,
263
+ ) -> dict[str, Any]:
264
+ self._ensure_auth()
265
+ response = self._request_with_retries(
266
+ "POST",
267
+ f"{CHATGPT_BASE_URL}{path}",
268
+ json=body,
269
+ timeout=self._timeout if not _is_given(timeout) else timeout,
270
+ )
222
271
  return response.json()
223
272
 
273
+ def _request_with_retries(self, method: str, url: str, **kwargs: Any) -> requests.Response:
274
+ last_error: requests.RequestException | None = None
275
+ use_session = kwargs.pop("_use_session", True)
276
+ for attempt in range(self._max_retries + 1):
277
+ try:
278
+ request = self._session.request if use_session else requests.request
279
+ response = request(method, url, **kwargs)
280
+ if self._should_retry_response(response, attempt):
281
+ self._sleep_before_retry(response, attempt)
282
+ continue
283
+ response.raise_for_status()
284
+ return response
285
+ except (requests.Timeout, requests.ConnectionError) as exc:
286
+ last_error = exc
287
+ if attempt >= self._max_retries:
288
+ raise
289
+ self._sleep_before_retry(None, attempt)
290
+ if last_error is not None:
291
+ raise last_error
292
+ raise RuntimeError("Request retry loop exhausted")
293
+
294
+ def _should_retry_response(self, response: requests.Response, attempt: int) -> bool:
295
+ if attempt >= self._max_retries:
296
+ return False
297
+ return response.status_code == 429 or 500 <= response.status_code < 600
298
+
299
+ def _sleep_before_retry(self, response: requests.Response | None, attempt: int) -> None:
300
+ retry_after = response.headers.get("Retry-After") if response is not None else None
301
+ delay = _parse_retry_after(retry_after)
302
+ if delay is None:
303
+ delay = self._retry_base_delay * (2 ** attempt)
304
+ time.sleep(delay)
305
+
224
306
  def realtime_websocket_url(self, *, model: str) -> str:
225
307
  """Return the official OpenAI Realtime WebSocket URL for Codex plugins."""
226
308
  if not model:
@@ -242,3 +324,12 @@ class CodexClient:
242
324
 
243
325
 
244
326
  OpenAI = CodexClient
327
+
328
+
329
+ def _parse_retry_after(value: str | None) -> float | None:
330
+ if not value:
331
+ return None
332
+ try:
333
+ return max(0.0, float(value))
334
+ except ValueError:
335
+ return None
@@ -173,6 +173,35 @@ class Transcription(CodexBaseModel):
173
173
  text: str = ""
174
174
 
175
175
 
176
+ class RawMemoryMetadata(CodexBaseModel):
177
+ source_path: str
178
+
179
+
180
+ class RawMemory(CodexBaseModel):
181
+ id: str
182
+ metadata: RawMemoryMetadata
183
+ items: list[Any] = Field(default_factory=list)
184
+
185
+
186
+ class MemorySummarizeOutput(CodexBaseModel):
187
+ raw_memory: str = Field(default="", alias="trace_summary")
188
+ memory_summary: str = ""
189
+
190
+
191
+ class MemorySummarizeResponse(CodexBaseModel):
192
+ output: list[MemorySummarizeOutput] = Field(default_factory=list)
193
+
194
+
195
+ class UploadedFile(CodexBaseModel):
196
+ file_id: str
197
+ uri: str
198
+ download_url: str
199
+ file_name: str
200
+ file_size_bytes: int
201
+ mime_type: Optional[str] = None
202
+ path: str
203
+
204
+
176
205
  class CompactedResponse(CodexBaseModel):
177
206
  id: str
178
207
  object: str = "response.compacted"
@@ -14,6 +14,10 @@ from ._models import (
14
14
  Embedding,
15
15
  EmbeddingUsage,
16
16
  Model,
17
+ MemorySummarizeOutput,
18
+ MemorySummarizeResponse,
19
+ RawMemory,
20
+ RawMemoryMetadata,
17
21
  ReasoningEffort,
18
22
  ReasoningSummary,
19
23
  RealtimeCallResponse,
@@ -24,6 +28,7 @@ from ._models import (
24
28
  SyncPage,
25
29
  TokenDetails,
26
30
  Transcription,
31
+ UploadedFile,
27
32
  Verbosity,
28
33
  )
29
34
  from ._utils import CodexBackendUnsupportedParameterError, image_b64, image_url
@@ -36,8 +41,12 @@ __all__ = [
36
41
  "Embedding",
37
42
  "EmbeddingUsage",
38
43
  "CompactedResponse",
44
+ "MemorySummarizeOutput",
45
+ "MemorySummarizeResponse",
39
46
  "Model",
40
47
  "OpenAI",
48
+ "RawMemory",
49
+ "RawMemoryMetadata",
41
50
  "ReasoningEffort",
42
51
  "ReasoningSummary",
43
52
  "Response",
@@ -48,6 +57,7 @@ __all__ = [
48
57
  "SyncPage",
49
58
  "TokenDetails",
50
59
  "Transcription",
60
+ "UploadedFile",
51
61
  "Verbosity",
52
62
  "image_b64",
53
63
  "image_url",
@@ -4,6 +4,9 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any, TYPE_CHECKING
6
6
 
7
+ from .._models import MemorySummarizeResponse
8
+ from .._utils import _jsonable
9
+
7
10
  if TYPE_CHECKING:
8
11
  from .._client import CodexClient
9
12
 
@@ -16,6 +19,7 @@ class CodexResources:
16
19
  self.memories = CodexMemories(client)
17
20
  self.tasks = CodexTasks(client)
18
21
  self.environments = CodexEnvironments(client)
22
+ self.config = CodexConfig(client)
19
23
  self.user_system_messages = CodexUserSystemMessages(client)
20
24
 
21
25
  def usage(self) -> dict[str, Any]:
@@ -31,6 +35,32 @@ class CodexMemories:
31
35
  def list(self) -> dict[str, Any]:
32
36
  return self._client._get_chatgpt("/memories")
33
37
 
38
+ def trace_summarize(
39
+ self,
40
+ *,
41
+ model: str,
42
+ traces: list[Any],
43
+ reasoning: Any = None,
44
+ ) -> MemorySummarizeResponse:
45
+ if not model:
46
+ raise ValueError(f"Expected a non-empty value for `model` but received {model!r}")
47
+ payload: dict[str, Any] = {"model": model, "traces": _jsonable(traces)}
48
+ if reasoning is not None:
49
+ payload["reasoning"] = _jsonable(reasoning)
50
+ return MemorySummarizeResponse.model_validate(
51
+ self._client._post("/memories/trace_summarize", body=payload).json()
52
+ )
53
+
54
+
55
+ class CodexConfig:
56
+ """Codex WHAM account configuration resources."""
57
+
58
+ def __init__(self, client: CodexClient) -> None:
59
+ self._client = client
60
+
61
+ def requirements(self) -> dict[str, Any]:
62
+ return self._client._get_wham("/wham/config/requirements")
63
+
34
64
 
35
65
  class CodexTasks:
36
66
  """Codex cloud task data for the authenticated account."""