codex-backend-sdk 0.3.2__tar.gz → 0.3.4__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.4/CHANGELOG.md +13 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/PKG-INFO +33 -1
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/README.md +32 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/__init__.py +5 -1
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_client.py +35 -41
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_models.py +53 -1
- codex_backend_sdk-0.3.4/codex_backend_sdk/_transport.py +71 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_utils.py +3 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/codex_client.py +4 -0
- codex_backend_sdk-0.3.2/codex_backend_sdk/resources/responses.py → codex_backend_sdk-0.3.4/codex_backend_sdk/resources/_responses_payloads.py +114 -176
- codex_backend_sdk-0.3.4/codex_backend_sdk/resources/responses.py +224 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/docs/backend-api.md +4 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/pyproject.toml +1 -1
- codex_backend_sdk-0.3.4/tests/test_account_info.py +68 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_client_retry.py +5 -5
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_responses_resource.py +102 -1
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/.gitignore +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/LICENSE +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/codex.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/models.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/openai_oauth.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/realtime.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/examples/agent.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_codex_resources.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_openai_oauth_resources.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_realtime_resource.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_structured_output.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_tools.py +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.3.4] - 2026-05-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `authenticate(interactive=False)` to allow non-interactive credential checks without triggering the browser OAuth flow.
|
|
9
|
+
- Added `client.authenticated` for simple auth-state introspection.
|
|
10
|
+
- Added `client.account_info()` to expose safe non-secret account metadata (`authenticated`, `account_id`, `email`, `plan_type`).
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
- Documented the non-interactive authentication helpers in `README.md`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-backend-sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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`,
|
|
@@ -331,6 +355,14 @@ headers = client.realtime_websocket_headers(session_id="voice-session")
|
|
|
331
355
|
`openai_api_key`. The default `authenticate(request_api_key=True)` flow stores
|
|
332
356
|
that key when available.
|
|
333
357
|
|
|
358
|
+
For non-interactive checks, you can avoid triggering a browser login flow:
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
client = OpenAI().authenticate(interactive=False)
|
|
362
|
+
print(client.authenticated)
|
|
363
|
+
print(client.account_info())
|
|
364
|
+
```
|
|
365
|
+
|
|
334
366
|
### Embeddings
|
|
335
367
|
|
|
336
368
|
`client.embeddings.create(...)` mirrors the official OpenAI embeddings resource
|
|
@@ -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`,
|
|
@@ -305,6 +329,14 @@ headers = client.realtime_websocket_headers(session_id="voice-session")
|
|
|
305
329
|
`openai_api_key`. The default `authenticate(request_api_key=True)` flow stores
|
|
306
330
|
that key when available.
|
|
307
331
|
|
|
332
|
+
For non-interactive checks, you can avoid triggering a browser login flow:
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
client = OpenAI().authenticate(interactive=False)
|
|
336
|
+
print(client.authenticated)
|
|
337
|
+
print(client.account_info())
|
|
338
|
+
```
|
|
339
|
+
|
|
308
340
|
### Embeddings
|
|
309
341
|
|
|
310
342
|
`client.embeddings.create(...)` mirrors the official OpenAI embeddings resource
|
|
@@ -17,7 +17,7 @@ Quickstart:
|
|
|
17
17
|
print(response.output_text)
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
__version__ = "0.3.
|
|
20
|
+
__version__ = "0.3.4"
|
|
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
|
|
@@ -38,7 +38,9 @@ from .codex_client import (
|
|
|
38
38
|
Model,
|
|
39
39
|
RawMemory,
|
|
40
40
|
RawMemoryMetadata,
|
|
41
|
+
ParsedResponse,
|
|
41
42
|
Response,
|
|
43
|
+
ResponseFormatJsonSchema,
|
|
42
44
|
ResponseStreamEvent,
|
|
43
45
|
ResponseUsage,
|
|
44
46
|
RealtimeCallResponse,
|
|
@@ -61,9 +63,11 @@ __all__ = [
|
|
|
61
63
|
"MemorySummarizeOutput",
|
|
62
64
|
"MemorySummarizeResponse",
|
|
63
65
|
"Model",
|
|
66
|
+
"ParsedResponse",
|
|
64
67
|
"RawMemory",
|
|
65
68
|
"RawMemoryMetadata",
|
|
66
69
|
"Response",
|
|
70
|
+
"ResponseFormatJsonSchema",
|
|
67
71
|
"ResponseStreamEvent",
|
|
68
72
|
"ResponseUsage",
|
|
69
73
|
"RealtimeCallResponse",
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import urllib.parse
|
|
6
|
-
import time
|
|
7
6
|
from typing import Any, Optional
|
|
8
7
|
|
|
9
8
|
import requests
|
|
10
9
|
|
|
10
|
+
from ._transport import request_with_retries
|
|
11
11
|
from ._utils import _UNSET, _is_given
|
|
12
12
|
from .storage import TokenStore, load_tokens, save_tokens, token_needs_refresh
|
|
13
13
|
|
|
@@ -62,7 +62,12 @@ class CodexClient:
|
|
|
62
62
|
self.files = Files(self)
|
|
63
63
|
self.codex = CodexResources(self)
|
|
64
64
|
|
|
65
|
-
def authenticate(
|
|
65
|
+
def authenticate(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
request_api_key: bool = True,
|
|
69
|
+
interactive: bool = True,
|
|
70
|
+
) -> "CodexClient":
|
|
66
71
|
from .oauth import refresh_access_token, run_oauth_flow
|
|
67
72
|
|
|
68
73
|
def refresh(store: TokenStore) -> Optional[TokenStore]:
|
|
@@ -87,9 +92,28 @@ class CodexClient:
|
|
|
87
92
|
self._set_store(store)
|
|
88
93
|
return self
|
|
89
94
|
|
|
95
|
+
if not interactive:
|
|
96
|
+
raise RuntimeError("No usable stored Codex credentials; interactive login required.")
|
|
97
|
+
|
|
90
98
|
self._set_store(run_oauth_flow(request_api_key=request_api_key))
|
|
91
99
|
return self
|
|
92
100
|
|
|
101
|
+
@property
|
|
102
|
+
def authenticated(self) -> bool:
|
|
103
|
+
"""Whether this client currently has OAuth credentials loaded."""
|
|
104
|
+
return bool(self._store and self._store.account_id)
|
|
105
|
+
|
|
106
|
+
def account_info(self) -> dict[str, Any]:
|
|
107
|
+
"""Return safe non-secret account metadata decoded from stored tokens."""
|
|
108
|
+
store = self._store
|
|
109
|
+
authenticated = bool(store and store.account_id)
|
|
110
|
+
return {
|
|
111
|
+
"authenticated": authenticated,
|
|
112
|
+
"account_id": store.account_id if store else None,
|
|
113
|
+
"email": store.email if store else None,
|
|
114
|
+
"plan_type": store.plan_type if store else None,
|
|
115
|
+
}
|
|
116
|
+
|
|
93
117
|
def _set_store(self, store: TokenStore) -> None:
|
|
94
118
|
self._store = store
|
|
95
119
|
self._session.headers.update(self._auth_headers())
|
|
@@ -271,37 +295,16 @@ class CodexClient:
|
|
|
271
295
|
return response.json()
|
|
272
296
|
|
|
273
297
|
def _request_with_retries(self, method: str, url: str, **kwargs: Any) -> requests.Response:
|
|
274
|
-
last_error: requests.RequestException | None = None
|
|
275
298
|
use_session = kwargs.pop("_use_session", True)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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)
|
|
299
|
+
return request_with_retries(
|
|
300
|
+
self._session,
|
|
301
|
+
method,
|
|
302
|
+
url,
|
|
303
|
+
max_retries=self._max_retries,
|
|
304
|
+
retry_base_delay=self._retry_base_delay,
|
|
305
|
+
use_session=use_session,
|
|
306
|
+
**kwargs,
|
|
307
|
+
)
|
|
305
308
|
|
|
306
309
|
def realtime_websocket_url(self, *, model: str) -> str:
|
|
307
310
|
"""Return the official OpenAI Realtime WebSocket URL for Codex plugins."""
|
|
@@ -324,12 +327,3 @@ class CodexClient:
|
|
|
324
327
|
|
|
325
328
|
|
|
326
329
|
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
|
|
@@ -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
|
|
@@ -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):
|
|
@@ -16,12 +16,14 @@ from ._models import (
|
|
|
16
16
|
Model,
|
|
17
17
|
MemorySummarizeOutput,
|
|
18
18
|
MemorySummarizeResponse,
|
|
19
|
+
ParsedResponse,
|
|
19
20
|
RawMemory,
|
|
20
21
|
RawMemoryMetadata,
|
|
21
22
|
ReasoningEffort,
|
|
22
23
|
ReasoningSummary,
|
|
23
24
|
RealtimeCallResponse,
|
|
24
25
|
Response,
|
|
26
|
+
ResponseFormatJsonSchema,
|
|
25
27
|
ResponseStreamEvent,
|
|
26
28
|
ResponseUsage,
|
|
27
29
|
ServiceTier,
|
|
@@ -45,11 +47,13 @@ __all__ = [
|
|
|
45
47
|
"MemorySummarizeResponse",
|
|
46
48
|
"Model",
|
|
47
49
|
"OpenAI",
|
|
50
|
+
"ParsedResponse",
|
|
48
51
|
"RawMemory",
|
|
49
52
|
"RawMemoryMetadata",
|
|
50
53
|
"ReasoningEffort",
|
|
51
54
|
"ReasoningSummary",
|
|
52
55
|
"Response",
|
|
56
|
+
"ResponseFormatJsonSchema",
|
|
53
57
|
"ResponseStreamEvent",
|
|
54
58
|
"ResponseUsage",
|
|
55
59
|
"RealtimeCallResponse",
|