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.
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/PKG-INFO +40 -9
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/README.md +39 -8
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/__init__.py +13 -1
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_client.py +54 -13
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_models.py +72 -1
- codex_backend_sdk-0.3.3/codex_backend_sdk/_transport.py +71 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_utils.py +3 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/codex_client.py +12 -0
- 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
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/codex.py +11 -6
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/models.py +3 -2
- codex_backend_sdk-0.3.3/codex_backend_sdk/resources/responses.py +224 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/docs/backend-api.md +11 -3
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/pyproject.toml +1 -1
- codex_backend_sdk-0.3.3/tests/test_client_retry.py +97 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_codex_resources.py +12 -8
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_responses_resource.py +129 -23
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/.gitignore +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/LICENSE +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/openai_oauth.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/realtime.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/examples/agent.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_openai_oauth_resources.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_realtime_resource.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.3.1 → codex_backend_sdk-0.3.3}/tests/test_structured_output.py +0 -0
- {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.
|
|
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
|
|
417
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
391
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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.
|
|
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.
|
|
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,26 @@ 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
|
+
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",
|