codex-backend-sdk 0.3.1__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.
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/PKG-INFO +16 -9
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/README.md +15 -8
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/__init__.py +9 -1
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_client.py +84 -13
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_models.py +19 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/codex_client.py +8 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/codex.py +11 -6
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/models.py +3 -2
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/docs/backend-api.md +7 -3
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/pyproject.toml +1 -1
- codex_backend_sdk-0.3.2/tests/test_client_retry.py +97 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_codex_resources.py +12 -8
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_responses_resource.py +27 -22
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/.gitignore +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/LICENSE +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/_utils.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/openai_oauth.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/realtime.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/responses.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/examples/agent.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_openai_oauth_resources.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_realtime_resource.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/tests/test_structured_output.py +0 -0
- {codex_backend_sdk-0.3.1 → 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.
|
|
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
|
|
@@ -270,10 +270,11 @@ as opaque backend state.
|
|
|
270
270
|
|
|
271
271
|
`client.models.list()` and `client.models.retrieve(model)` mirror the official
|
|
272
272
|
OpenAI models resource, while preserving Codex-specific metadata as extra
|
|
273
|
-
Pydantic fields.
|
|
273
|
+
Pydantic fields. The returned page also exposes the backend `ETag` when present.
|
|
274
274
|
|
|
275
275
|
```python
|
|
276
276
|
models = client.models.list()
|
|
277
|
+
print(models.etag)
|
|
277
278
|
for model in models:
|
|
278
279
|
print(
|
|
279
280
|
model.id,
|
|
@@ -413,23 +414,29 @@ These methods return raw backend dictionaries because these payloads can contain
|
|
|
413
414
|
personal account-specific fields and may change without notice.
|
|
414
415
|
|
|
415
416
|
`client.codex.memories.trace_summarize(...)` exposes the Codex memory
|
|
416
|
-
summarization endpoint used by the official client. It
|
|
417
|
-
|
|
417
|
+
summarization endpoint used by the official client. It accepts dictionaries or
|
|
418
|
+
`RawMemory` objects and returns a typed `MemorySummarizeResponse`:
|
|
418
419
|
|
|
419
420
|
```python
|
|
421
|
+
from codex_backend_sdk import RawMemory
|
|
422
|
+
|
|
420
423
|
summary = client.codex.memories.trace_summarize(
|
|
421
424
|
model="gpt-5.4",
|
|
422
425
|
traces=[
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
426
|
+
RawMemory(
|
|
427
|
+
id="trace_1",
|
|
428
|
+
metadata={"source_path": "memory.jsonl"},
|
|
429
|
+
items=[{"type": "message", "content": "Remember this"}],
|
|
430
|
+
)
|
|
428
431
|
],
|
|
429
432
|
reasoning={"effort": "low"},
|
|
430
433
|
)
|
|
434
|
+
print(summary.output[0].memory_summary)
|
|
431
435
|
```
|
|
432
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
|
+
|
|
433
440
|
### File Uploads
|
|
434
441
|
|
|
435
442
|
`client.files.upload(...)` follows the official Codex file flow for Apps/MCP
|
|
@@ -244,10 +244,11 @@ as opaque backend state.
|
|
|
244
244
|
|
|
245
245
|
`client.models.list()` and `client.models.retrieve(model)` mirror the official
|
|
246
246
|
OpenAI models resource, while preserving Codex-specific metadata as extra
|
|
247
|
-
Pydantic fields.
|
|
247
|
+
Pydantic fields. The returned page also exposes the backend `ETag` when present.
|
|
248
248
|
|
|
249
249
|
```python
|
|
250
250
|
models = client.models.list()
|
|
251
|
+
print(models.etag)
|
|
251
252
|
for model in models:
|
|
252
253
|
print(
|
|
253
254
|
model.id,
|
|
@@ -387,23 +388,29 @@ These methods return raw backend dictionaries because these payloads can contain
|
|
|
387
388
|
personal account-specific fields and may change without notice.
|
|
388
389
|
|
|
389
390
|
`client.codex.memories.trace_summarize(...)` exposes the Codex memory
|
|
390
|
-
summarization endpoint used by the official client. It
|
|
391
|
-
|
|
391
|
+
summarization endpoint used by the official client. It accepts dictionaries or
|
|
392
|
+
`RawMemory` objects and returns a typed `MemorySummarizeResponse`:
|
|
392
393
|
|
|
393
394
|
```python
|
|
395
|
+
from codex_backend_sdk import RawMemory
|
|
396
|
+
|
|
394
397
|
summary = client.codex.memories.trace_summarize(
|
|
395
398
|
model="gpt-5.4",
|
|
396
399
|
traces=[
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
RawMemory(
|
|
401
|
+
id="trace_1",
|
|
402
|
+
metadata={"source_path": "memory.jsonl"},
|
|
403
|
+
items=[{"type": "message", "content": "Remember this"}],
|
|
404
|
+
)
|
|
402
405
|
],
|
|
403
406
|
reasoning={"effort": "low"},
|
|
404
407
|
)
|
|
408
|
+
print(summary.output[0].memory_summary)
|
|
405
409
|
```
|
|
406
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
|
+
|
|
407
414
|
### File Uploads
|
|
408
415
|
|
|
409
416
|
`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.
|
|
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,
|
|
@@ -54,7 +58,11 @@ __all__ = [
|
|
|
54
58
|
"CreateEmbeddingResponse",
|
|
55
59
|
"Embedding",
|
|
56
60
|
"EmbeddingUsage",
|
|
61
|
+
"MemorySummarizeOutput",
|
|
62
|
+
"MemorySummarizeResponse",
|
|
57
63
|
"Model",
|
|
64
|
+
"RawMemory",
|
|
65
|
+
"RawMemoryMetadata",
|
|
58
66
|
"Response",
|
|
59
67
|
"ResponseStreamEvent",
|
|
60
68
|
"ResponseUsage",
|
|
@@ -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,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.
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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.
|
|
219
|
-
|
|
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.
|
|
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,47 @@ class CodexClient:
|
|
|
233
262
|
timeout: Any = _UNSET,
|
|
234
263
|
) -> dict[str, Any]:
|
|
235
264
|
self._ensure_auth()
|
|
236
|
-
response = self.
|
|
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
|
+
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
|
+
|
|
244
306
|
def realtime_websocket_url(self, *, model: str) -> str:
|
|
245
307
|
"""Return the official OpenAI Realtime WebSocket URL for Codex plugins."""
|
|
246
308
|
if not model:
|
|
@@ -262,3 +324,12 @@ class CodexClient:
|
|
|
262
324
|
|
|
263
325
|
|
|
264
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,25 @@ 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
|
+
|
|
176
195
|
class UploadedFile(CodexBaseModel):
|
|
177
196
|
file_id: str
|
|
178
197
|
uri: str
|
|
@@ -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,
|
|
@@ -37,8 +41,12 @@ __all__ = [
|
|
|
37
41
|
"Embedding",
|
|
38
42
|
"EmbeddingUsage",
|
|
39
43
|
"CompactedResponse",
|
|
44
|
+
"MemorySummarizeOutput",
|
|
45
|
+
"MemorySummarizeResponse",
|
|
40
46
|
"Model",
|
|
41
47
|
"OpenAI",
|
|
48
|
+
"RawMemory",
|
|
49
|
+
"RawMemoryMetadata",
|
|
42
50
|
"ReasoningEffort",
|
|
43
51
|
"ReasoningSummary",
|
|
44
52
|
"Response",
|
|
@@ -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
|
|
|
@@ -36,15 +39,17 @@ class CodexMemories:
|
|
|
36
39
|
self,
|
|
37
40
|
*,
|
|
38
41
|
model: str,
|
|
39
|
-
traces: list[
|
|
40
|
-
reasoning:
|
|
41
|
-
) ->
|
|
42
|
+
traces: list[Any],
|
|
43
|
+
reasoning: Any = None,
|
|
44
|
+
) -> MemorySummarizeResponse:
|
|
42
45
|
if not model:
|
|
43
46
|
raise ValueError(f"Expected a non-empty value for `model` but received {model!r}")
|
|
44
|
-
payload: dict[str, Any] = {"model": model, "traces": traces}
|
|
47
|
+
payload: dict[str, Any] = {"model": model, "traces": _jsonable(traces)}
|
|
45
48
|
if reasoning is not None:
|
|
46
|
-
payload["reasoning"] = reasoning
|
|
47
|
-
return
|
|
49
|
+
payload["reasoning"] = _jsonable(reasoning)
|
|
50
|
+
return MemorySummarizeResponse.model_validate(
|
|
51
|
+
self._client._post("/memories/trace_summarize", body=payload).json()
|
|
52
|
+
)
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
class CodexConfig:
|
|
@@ -36,10 +36,11 @@ class Models:
|
|
|
36
36
|
if time.time() - self._cache_fetched_at <= _CACHE_TTL:
|
|
37
37
|
return self._cache
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
response = self._client._get_raw("/models", params={"client_version": CLIENT_VERSION})
|
|
40
|
+
data = response.json()
|
|
40
41
|
models = [_model_from_backend(item) for item in data.get("models", [])]
|
|
41
42
|
models.sort(key=lambda model: getattr(model, "priority", 0))
|
|
42
|
-
page = SyncPage(data=models)
|
|
43
|
+
page = SyncPage(data=models, etag=response.headers.get("ETag"))
|
|
43
44
|
self._cache = page
|
|
44
45
|
self._cache_fetched_at = time.time()
|
|
45
46
|
return page
|
|
@@ -47,6 +47,9 @@ List models available to this account.
|
|
|
47
47
|
|
|
48
48
|
**Response** — JSON `{ "models": [ ModelObject, … ] }`
|
|
49
49
|
|
|
50
|
+
The backend may include an `ETag` header. The SDK preserves it as
|
|
51
|
+
`client.models.list().etag`.
|
|
52
|
+
|
|
50
53
|
Key fields per model:
|
|
51
54
|
| Field | Type | Notes |
|
|
52
55
|
|---|---|---|
|
|
@@ -228,8 +231,8 @@ Summarize traces into persistent memories.
|
|
|
228
231
|
|
|
229
232
|
**SDK method**: `client.codex.memories.trace_summarize(...)`
|
|
230
233
|
|
|
231
|
-
**Status**: Supported as a
|
|
232
|
-
plan/account capabilities.
|
|
234
|
+
**Status**: Supported as a typed helper. May return `403 Forbidden` depending
|
|
235
|
+
on plan/account capabilities.
|
|
233
236
|
|
|
234
237
|
**Request body**:
|
|
235
238
|
```json
|
|
@@ -246,7 +249,8 @@ plan/account capabilities.
|
|
|
246
249
|
}
|
|
247
250
|
```
|
|
248
251
|
|
|
249
|
-
**Response** —
|
|
252
|
+
**Response** — JSON `{ "output": [...] }`, exposed by the SDK as
|
|
253
|
+
`MemorySummarizeResponse`.
|
|
250
254
|
|
|
251
255
|
---
|
|
252
256
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
import codex_backend_sdk._client as client_module
|
|
4
|
+
from codex_backend_sdk import OpenAI
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DummySession:
|
|
8
|
+
def __init__(self, outcomes):
|
|
9
|
+
self.outcomes = list(outcomes)
|
|
10
|
+
self.calls = []
|
|
11
|
+
|
|
12
|
+
def request(self, method, url, **kwargs):
|
|
13
|
+
self.calls.append((method, url, kwargs))
|
|
14
|
+
outcome = self.outcomes.pop(0)
|
|
15
|
+
if isinstance(outcome, Exception):
|
|
16
|
+
raise outcome
|
|
17
|
+
return outcome
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RetryClient(OpenAI):
|
|
21
|
+
def __init__(self, outcomes, **kwargs):
|
|
22
|
+
super().__init__(max_retries=kwargs.pop("max_retries", 2), retry_base_delay=0, **kwargs)
|
|
23
|
+
self._session = DummySession(outcomes)
|
|
24
|
+
|
|
25
|
+
def _ensure_auth(self):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _response(status_code, *, body=b"{}", headers=None):
|
|
30
|
+
response = requests.Response()
|
|
31
|
+
response.status_code = status_code
|
|
32
|
+
response._content = body
|
|
33
|
+
response.headers.update(headers or {})
|
|
34
|
+
response.url = "https://example.test"
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_retry_retries_5xx_then_returns_success(monkeypatch):
|
|
39
|
+
sleeps = []
|
|
40
|
+
monkeypatch.setattr(client_module.time, "sleep", sleeps.append)
|
|
41
|
+
client = RetryClient([
|
|
42
|
+
_response(503),
|
|
43
|
+
_response(200, body=b'{"ok": true}'),
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
response = client._get_raw("/models")
|
|
47
|
+
|
|
48
|
+
assert response.json() == {"ok": True}
|
|
49
|
+
assert len(client._session.calls) == 2
|
|
50
|
+
assert sleeps == [0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_retry_honors_retry_after_header(monkeypatch):
|
|
54
|
+
sleeps = []
|
|
55
|
+
monkeypatch.setattr(client_module.time, "sleep", sleeps.append)
|
|
56
|
+
client = RetryClient([
|
|
57
|
+
_response(429, headers={"Retry-After": "1.5"}),
|
|
58
|
+
_response(200),
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
client._get_raw("/models")
|
|
62
|
+
|
|
63
|
+
assert len(client._session.calls) == 2
|
|
64
|
+
assert sleeps == [1.5]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_retry_does_not_retry_client_errors(monkeypatch):
|
|
68
|
+
sleeps = []
|
|
69
|
+
monkeypatch.setattr(client_module.time, "sleep", sleeps.append)
|
|
70
|
+
client = RetryClient([
|
|
71
|
+
_response(400, body=b'{"error": "bad request"}'),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
client._get_raw("/models")
|
|
76
|
+
except requests.HTTPError:
|
|
77
|
+
pass
|
|
78
|
+
else:
|
|
79
|
+
raise AssertionError("Expected HTTPError")
|
|
80
|
+
|
|
81
|
+
assert len(client._session.calls) == 1
|
|
82
|
+
assert sleeps == []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_retry_retries_transport_timeout(monkeypatch):
|
|
86
|
+
sleeps = []
|
|
87
|
+
monkeypatch.setattr(client_module.time, "sleep", sleeps.append)
|
|
88
|
+
client = RetryClient([
|
|
89
|
+
requests.Timeout("temporary timeout"),
|
|
90
|
+
_response(200, body=b'{"ok": true}'),
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
response = client._get_raw("/models")
|
|
94
|
+
|
|
95
|
+
assert response.json() == {"ok": True}
|
|
96
|
+
assert len(client._session.calls) == 2
|
|
97
|
+
assert sleeps == [0]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from codex_backend_sdk import OpenAI
|
|
1
|
+
from codex_backend_sdk import MemorySummarizeResponse, OpenAI, RawMemory
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class FakeCodexClient(OpenAI):
|
|
@@ -41,7 +41,9 @@ class FakeCodexClient(OpenAI):
|
|
|
41
41
|
def _post(self, path, *, body, stream=False):
|
|
42
42
|
self.posts.append((path, body, stream))
|
|
43
43
|
if path == "/memories/trace_summarize":
|
|
44
|
-
return FakeJSONResponse({
|
|
44
|
+
return FakeJSONResponse({
|
|
45
|
+
"output": [{"trace_summary": "raw", "memory_summary": "summary"}]
|
|
46
|
+
})
|
|
45
47
|
raise AssertionError(f"Unexpected Codex post path: {path}")
|
|
46
48
|
|
|
47
49
|
|
|
@@ -77,16 +79,18 @@ def test_codex_memories_trace_summarize_posts_codex_payload():
|
|
|
77
79
|
response = client.codex.memories.trace_summarize(
|
|
78
80
|
model="gpt-test",
|
|
79
81
|
traces=[
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
RawMemory(
|
|
83
|
+
id="trace_1",
|
|
84
|
+
metadata={"source_path": "/tmp/memory.jsonl"},
|
|
85
|
+
items=[{"type": "message", "content": "remember me"}],
|
|
86
|
+
)
|
|
85
87
|
],
|
|
86
88
|
reasoning={"effort": "low"},
|
|
87
89
|
)
|
|
88
90
|
|
|
89
|
-
assert response
|
|
91
|
+
assert isinstance(response, MemorySummarizeResponse)
|
|
92
|
+
assert response.output[0].raw_memory == "raw"
|
|
93
|
+
assert response.output[0].memory_summary == "summary"
|
|
90
94
|
assert client.posts == [
|
|
91
95
|
(
|
|
92
96
|
"/memories/trace_summarize",
|
|
@@ -15,8 +15,9 @@ class FakeSSE:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class FakeJSONResponse:
|
|
18
|
-
def __init__(self, payload):
|
|
18
|
+
def __init__(self, payload, headers=None):
|
|
19
19
|
self._payload = payload
|
|
20
|
+
self.headers = headers or {}
|
|
20
21
|
|
|
21
22
|
def json(self):
|
|
22
23
|
return self._payload
|
|
@@ -47,28 +48,31 @@ class FakeClient(OpenAI):
|
|
|
47
48
|
),
|
|
48
49
|
])
|
|
49
50
|
|
|
50
|
-
def
|
|
51
|
+
def _get_raw(self, path, *, params=None, headers=None, timeout=None):
|
|
51
52
|
self.gets.append((path, params))
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
53
|
+
return FakeJSONResponse(
|
|
54
|
+
{
|
|
55
|
+
"models": [
|
|
56
|
+
{
|
|
57
|
+
"slug": "gpt-lower-priority",
|
|
58
|
+
"display_name": "GPT Lower Priority",
|
|
59
|
+
"description": "Test model",
|
|
60
|
+
"context_window": 123,
|
|
61
|
+
"supported_in_api": True,
|
|
62
|
+
"priority": 7,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"slug": "gpt-test",
|
|
66
|
+
"display_name": "GPT Test",
|
|
67
|
+
"description": "Test model",
|
|
68
|
+
"context_window": 456,
|
|
69
|
+
"supported_in_api": True,
|
|
70
|
+
"priority": 1,
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
headers={"ETag": "models-etag"},
|
|
75
|
+
)
|
|
72
76
|
|
|
73
77
|
|
|
74
78
|
def test_responses_create_collects_to_pydantic_response():
|
|
@@ -147,6 +151,7 @@ def test_models_resource_returns_iterable_page():
|
|
|
147
151
|
assert len(page) == 2
|
|
148
152
|
assert model.id == "gpt-test"
|
|
149
153
|
assert model.context_window == 456
|
|
154
|
+
assert page.etag == "models-etag"
|
|
150
155
|
assert client.models.retrieve("gpt-test").id == "gpt-test"
|
|
151
156
|
assert len(client.gets) == 1
|
|
152
157
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/openai_oauth.py
RENAMED
|
File without changes
|
|
File without changes
|
{codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.2}/codex_backend_sdk/resources/responses.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|