codex-backend-sdk 0.3.0__tar.gz → 0.3.1__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.0 → codex_backend_sdk-0.3.1}/PKG-INFO +35 -4
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/README.md +34 -3
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/__init__.py +3 -1
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/_client.py +21 -1
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/_models.py +10 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/codex_client.py +2 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/codex.py +25 -0
- codex_backend_sdk-0.3.1/codex_backend_sdk/resources/files.py +116 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/models.py +17 -3
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/realtime.py +5 -11
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/responses.py +19 -2
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/docs/backend-api.md +53 -3
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/pyproject.toml +1 -1
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_codex_resources.py +61 -0
- codex_backend_sdk-0.3.1/tests/test_files_resource.py +114 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_realtime_resource.py +6 -4
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_responses_resource.py +73 -5
- codex_backend_sdk-0.3.0/tools/probe_v1_endpoints.py +0 -269
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/.gitignore +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/LICENSE +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/_utils.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/openai_oauth.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/examples/agent.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_openai_oauth_resources.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/tests/test_structured_output.py +0 -0
- {codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/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.1
|
|
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
|
|
|
@@ -403,18 +406,46 @@ not part of the official OpenAI SDK.
|
|
|
403
406
|
```python
|
|
404
407
|
memories = client.codex.memories.list()
|
|
405
408
|
customization = client.codex.user_system_messages.retrieve()
|
|
409
|
+
requirements = client.codex.config.requirements()
|
|
406
410
|
```
|
|
407
411
|
|
|
408
|
-
|
|
412
|
+
These methods return raw backend dictionaries because these payloads can contain
|
|
409
413
|
personal account-specific fields and may change without notice.
|
|
410
414
|
|
|
415
|
+
`client.codex.memories.trace_summarize(...)` exposes the Codex memory
|
|
416
|
+
summarization endpoint used by the official client. It returns the raw backend
|
|
417
|
+
dictionary:
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
summary = client.codex.memories.trace_summarize(
|
|
421
|
+
model="gpt-5.4",
|
|
422
|
+
traces=[
|
|
423
|
+
{
|
|
424
|
+
"id": "trace_1",
|
|
425
|
+
"metadata": {"source_path": "memory.jsonl"},
|
|
426
|
+
"items": [{"type": "message", "content": "Remember this"}],
|
|
427
|
+
}
|
|
428
|
+
],
|
|
429
|
+
reasoning={"effort": "low"},
|
|
430
|
+
)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### File Uploads
|
|
434
|
+
|
|
435
|
+
`client.files.upload(...)` follows the official Codex file flow for Apps/MCP
|
|
436
|
+
file parameters: create file metadata under ChatGPT, upload bytes to the signed
|
|
437
|
+
URL, then finalize the upload.
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
uploaded = client.files.upload("report.csv")
|
|
441
|
+
print(uploaded.uri) # sediment://file_...
|
|
442
|
+
```
|
|
443
|
+
|
|
411
444
|
### Observed But Not Exposed
|
|
412
445
|
|
|
413
446
|
The reverse-engineering notes in `docs/backend-api.md` include additional
|
|
414
447
|
observed endpoints. They are not exposed as SDK resources yet because they are
|
|
415
448
|
plan-gated, unavailable on `chatgpt.com`, or not stable enough:
|
|
416
449
|
|
|
417
|
-
- `POST /backend-api/codex/memories/trace_summarize`
|
|
418
|
-
- `GET /backend-api/wham/config/requirements`
|
|
419
450
|
- `POST /v1/audio/speech` (auth reaches the endpoint, but Pro OAuth lacks
|
|
420
451
|
`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
|
|
|
@@ -377,18 +380,46 @@ not part of the official OpenAI SDK.
|
|
|
377
380
|
```python
|
|
378
381
|
memories = client.codex.memories.list()
|
|
379
382
|
customization = client.codex.user_system_messages.retrieve()
|
|
383
|
+
requirements = client.codex.config.requirements()
|
|
380
384
|
```
|
|
381
385
|
|
|
382
|
-
|
|
386
|
+
These methods return raw backend dictionaries because these payloads can contain
|
|
383
387
|
personal account-specific fields and may change without notice.
|
|
384
388
|
|
|
389
|
+
`client.codex.memories.trace_summarize(...)` exposes the Codex memory
|
|
390
|
+
summarization endpoint used by the official client. It returns the raw backend
|
|
391
|
+
dictionary:
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
summary = client.codex.memories.trace_summarize(
|
|
395
|
+
model="gpt-5.4",
|
|
396
|
+
traces=[
|
|
397
|
+
{
|
|
398
|
+
"id": "trace_1",
|
|
399
|
+
"metadata": {"source_path": "memory.jsonl"},
|
|
400
|
+
"items": [{"type": "message", "content": "Remember this"}],
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
reasoning={"effort": "low"},
|
|
404
|
+
)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### File Uploads
|
|
408
|
+
|
|
409
|
+
`client.files.upload(...)` follows the official Codex file flow for Apps/MCP
|
|
410
|
+
file parameters: create file metadata under ChatGPT, upload bytes to the signed
|
|
411
|
+
URL, then finalize the upload.
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
uploaded = client.files.upload("report.csv")
|
|
415
|
+
print(uploaded.uri) # sediment://file_...
|
|
416
|
+
```
|
|
417
|
+
|
|
385
418
|
### Observed But Not Exposed
|
|
386
419
|
|
|
387
420
|
The reverse-engineering notes in `docs/backend-api.md` include additional
|
|
388
421
|
observed endpoints. They are not exposed as SDK resources yet because they are
|
|
389
422
|
plan-gated, unavailable on `chatgpt.com`, or not stable enough:
|
|
390
423
|
|
|
391
|
-
- `POST /backend-api/codex/memories/trace_summarize`
|
|
392
|
-
- `GET /backend-api/wham/config/requirements`
|
|
393
424
|
- `POST /v1/audio/speech` (auth reaches the endpoint, but Pro OAuth lacks
|
|
394
425
|
`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.
|
|
20
|
+
__version__ = "0.3.1"
|
|
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
|
|
@@ -41,6 +41,7 @@ from .codex_client import (
|
|
|
41
41
|
SyncPage,
|
|
42
42
|
CompactedResponse,
|
|
43
43
|
Transcription,
|
|
44
|
+
UploadedFile,
|
|
44
45
|
image_url,
|
|
45
46
|
image_b64,
|
|
46
47
|
)
|
|
@@ -61,6 +62,7 @@ __all__ = [
|
|
|
61
62
|
"SyncPage",
|
|
62
63
|
"CompactedResponse",
|
|
63
64
|
"Transcription",
|
|
65
|
+
"UploadedFile",
|
|
64
66
|
"ReasoningEffort",
|
|
65
67
|
"ReasoningSummary",
|
|
66
68
|
"ServiceTier",
|
|
@@ -36,6 +36,7 @@ class CodexClient:
|
|
|
36
36
|
from .resources.codex import CodexResources
|
|
37
37
|
from .resources.models import Models
|
|
38
38
|
from .resources.openai_oauth import Audio, Embeddings
|
|
39
|
+
from .resources.files import Files
|
|
39
40
|
from .resources.realtime import Realtime
|
|
40
41
|
from .resources.responses import Responses
|
|
41
42
|
|
|
@@ -53,6 +54,7 @@ class CodexClient:
|
|
|
53
54
|
self.realtime = Realtime(self)
|
|
54
55
|
self.embeddings = Embeddings(self)
|
|
55
56
|
self.audio = Audio(self)
|
|
57
|
+
self.files = Files(self)
|
|
56
58
|
self.codex = CodexResources(self)
|
|
57
59
|
|
|
58
60
|
def authenticate(self, *, request_api_key: bool = True) -> "CodexClient":
|
|
@@ -140,6 +142,7 @@ class CodexClient:
|
|
|
140
142
|
self,
|
|
141
143
|
path: str,
|
|
142
144
|
*,
|
|
145
|
+
body: Optional[dict[str, Any]] = None,
|
|
143
146
|
content: Optional[bytes] = None,
|
|
144
147
|
files: Any = None,
|
|
145
148
|
data: Any = None,
|
|
@@ -150,7 +153,8 @@ class CodexClient:
|
|
|
150
153
|
self._ensure_auth()
|
|
151
154
|
response = self._session.post(
|
|
152
155
|
f"{BASE_URL}{path}",
|
|
153
|
-
|
|
156
|
+
json=body if files is None and data is None and content is None else None,
|
|
157
|
+
data=content if content is not None else data,
|
|
154
158
|
files=files,
|
|
155
159
|
headers=headers,
|
|
156
160
|
params=params,
|
|
@@ -221,6 +225,22 @@ class CodexClient:
|
|
|
221
225
|
response.raise_for_status()
|
|
222
226
|
return response.json()
|
|
223
227
|
|
|
228
|
+
def _post_chatgpt(
|
|
229
|
+
self,
|
|
230
|
+
path: str,
|
|
231
|
+
*,
|
|
232
|
+
body: dict[str, Any],
|
|
233
|
+
timeout: Any = _UNSET,
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
self._ensure_auth()
|
|
236
|
+
response = self._session.post(
|
|
237
|
+
f"{CHATGPT_BASE_URL}{path}",
|
|
238
|
+
json=body,
|
|
239
|
+
timeout=self._timeout if not _is_given(timeout) else timeout,
|
|
240
|
+
)
|
|
241
|
+
response.raise_for_status()
|
|
242
|
+
return response.json()
|
|
243
|
+
|
|
224
244
|
def realtime_websocket_url(self, *, model: str) -> str:
|
|
225
245
|
"""Return the official OpenAI Realtime WebSocket URL for Codex plugins."""
|
|
226
246
|
if not model:
|
|
@@ -173,6 +173,16 @@ class Transcription(CodexBaseModel):
|
|
|
173
173
|
text: str = ""
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
class UploadedFile(CodexBaseModel):
|
|
177
|
+
file_id: str
|
|
178
|
+
uri: str
|
|
179
|
+
download_url: str
|
|
180
|
+
file_name: str
|
|
181
|
+
file_size_bytes: int
|
|
182
|
+
mime_type: Optional[str] = None
|
|
183
|
+
path: str
|
|
184
|
+
|
|
185
|
+
|
|
176
186
|
class CompactedResponse(CodexBaseModel):
|
|
177
187
|
id: str
|
|
178
188
|
object: str = "response.compacted"
|
|
@@ -24,6 +24,7 @@ from ._models import (
|
|
|
24
24
|
SyncPage,
|
|
25
25
|
TokenDetails,
|
|
26
26
|
Transcription,
|
|
27
|
+
UploadedFile,
|
|
27
28
|
Verbosity,
|
|
28
29
|
)
|
|
29
30
|
from ._utils import CodexBackendUnsupportedParameterError, image_b64, image_url
|
|
@@ -48,6 +49,7 @@ __all__ = [
|
|
|
48
49
|
"SyncPage",
|
|
49
50
|
"TokenDetails",
|
|
50
51
|
"Transcription",
|
|
52
|
+
"UploadedFile",
|
|
51
53
|
"Verbosity",
|
|
52
54
|
"image_b64",
|
|
53
55
|
"image_url",
|
|
@@ -16,6 +16,7 @@ class CodexResources:
|
|
|
16
16
|
self.memories = CodexMemories(client)
|
|
17
17
|
self.tasks = CodexTasks(client)
|
|
18
18
|
self.environments = CodexEnvironments(client)
|
|
19
|
+
self.config = CodexConfig(client)
|
|
19
20
|
self.user_system_messages = CodexUserSystemMessages(client)
|
|
20
21
|
|
|
21
22
|
def usage(self) -> dict[str, Any]:
|
|
@@ -31,6 +32,30 @@ class CodexMemories:
|
|
|
31
32
|
def list(self) -> dict[str, Any]:
|
|
32
33
|
return self._client._get_chatgpt("/memories")
|
|
33
34
|
|
|
35
|
+
def trace_summarize(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
model: str,
|
|
39
|
+
traces: list[dict[str, Any]],
|
|
40
|
+
reasoning: dict[str, Any] | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
if not model:
|
|
43
|
+
raise ValueError(f"Expected a non-empty value for `model` but received {model!r}")
|
|
44
|
+
payload: dict[str, Any] = {"model": model, "traces": traces}
|
|
45
|
+
if reasoning is not None:
|
|
46
|
+
payload["reasoning"] = reasoning
|
|
47
|
+
return self._client._post("/memories/trace_summarize", body=payload).json()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CodexConfig:
|
|
51
|
+
"""Codex WHAM account configuration resources."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, client: CodexClient) -> None:
|
|
54
|
+
self._client = client
|
|
55
|
+
|
|
56
|
+
def requirements(self) -> dict[str, Any]:
|
|
57
|
+
return self._client._get_wham("/wham/config/requirements")
|
|
58
|
+
|
|
34
59
|
|
|
35
60
|
class CodexTasks:
|
|
36
61
|
"""Codex cloud task data for the authenticated account."""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""ChatGPT file upload helpers used by Codex Apps tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .._models import UploadedFile
|
|
13
|
+
from .._utils import _UNSET, _is_given
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .._client import CodexClient
|
|
17
|
+
|
|
18
|
+
FILE_URI_PREFIX = "sediment://"
|
|
19
|
+
FILE_UPLOAD_LIMIT_BYTES = 512 * 1024 * 1024
|
|
20
|
+
FILE_USE_CASE = "codex"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Files:
|
|
24
|
+
"""File uploads for Codex Apps/MCP file parameters."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, client: CodexClient) -> None:
|
|
27
|
+
self._client = client
|
|
28
|
+
|
|
29
|
+
def upload(
|
|
30
|
+
self,
|
|
31
|
+
path: str | Path,
|
|
32
|
+
*,
|
|
33
|
+
timeout: Any = _UNSET,
|
|
34
|
+
finalize_timeout: float = 30,
|
|
35
|
+
finalize_retry_delay: float = 0.25,
|
|
36
|
+
) -> UploadedFile:
|
|
37
|
+
file_path = Path(path)
|
|
38
|
+
if not file_path.exists():
|
|
39
|
+
raise FileNotFoundError(f"path `{file_path}` does not exist")
|
|
40
|
+
if not file_path.is_file():
|
|
41
|
+
raise ValueError(f"path `{file_path}` is not a file")
|
|
42
|
+
|
|
43
|
+
size = file_path.stat().st_size
|
|
44
|
+
if size > FILE_UPLOAD_LIMIT_BYTES:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"file `{file_path}` is too large: {size} bytes exceeds "
|
|
47
|
+
f"the limit of {FILE_UPLOAD_LIMIT_BYTES} bytes"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
file_name = file_path.name or "file"
|
|
51
|
+
create_payload = self._client._post_chatgpt(
|
|
52
|
+
"/files",
|
|
53
|
+
body={"file_name": file_name, "file_size": size, "use_case": FILE_USE_CASE},
|
|
54
|
+
timeout=timeout,
|
|
55
|
+
)
|
|
56
|
+
file_id = create_payload["file_id"]
|
|
57
|
+
upload_url = create_payload["upload_url"]
|
|
58
|
+
|
|
59
|
+
with file_path.open("rb") as handle:
|
|
60
|
+
upload_response = requests.put(
|
|
61
|
+
upload_url,
|
|
62
|
+
data=handle,
|
|
63
|
+
headers={
|
|
64
|
+
"x-ms-blob-type": "BlockBlob",
|
|
65
|
+
"Content-Length": str(size),
|
|
66
|
+
},
|
|
67
|
+
timeout=self._request_timeout(timeout),
|
|
68
|
+
)
|
|
69
|
+
upload_response.raise_for_status()
|
|
70
|
+
|
|
71
|
+
finalize_payload = self._finalize_upload(
|
|
72
|
+
file_id,
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
finalize_timeout=finalize_timeout,
|
|
75
|
+
finalize_retry_delay=finalize_retry_delay,
|
|
76
|
+
)
|
|
77
|
+
return UploadedFile(
|
|
78
|
+
file_id=file_id,
|
|
79
|
+
uri=f"{FILE_URI_PREFIX}{file_id}",
|
|
80
|
+
download_url=finalize_payload["download_url"],
|
|
81
|
+
file_name=finalize_payload.get("file_name") or file_name,
|
|
82
|
+
file_size_bytes=size,
|
|
83
|
+
mime_type=finalize_payload.get("mime_type") or mimetypes.guess_type(file_name)[0],
|
|
84
|
+
path=str(file_path),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _finalize_upload(
|
|
88
|
+
self,
|
|
89
|
+
file_id: str,
|
|
90
|
+
*,
|
|
91
|
+
timeout: Any,
|
|
92
|
+
finalize_timeout: float,
|
|
93
|
+
finalize_retry_delay: float,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
deadline = time.monotonic() + finalize_timeout
|
|
96
|
+
while True:
|
|
97
|
+
payload = self._client._post_chatgpt(
|
|
98
|
+
f"/files/{file_id}/uploaded",
|
|
99
|
+
body={},
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
)
|
|
102
|
+
status = payload.get("status")
|
|
103
|
+
if status == "success":
|
|
104
|
+
if not payload.get("download_url"):
|
|
105
|
+
raise RuntimeError(f"OpenAI file upload for `{file_id}` failed: missing download_url")
|
|
106
|
+
return payload
|
|
107
|
+
if status == "retry" and time.monotonic() < deadline:
|
|
108
|
+
time.sleep(finalize_retry_delay)
|
|
109
|
+
continue
|
|
110
|
+
if status == "retry":
|
|
111
|
+
raise TimeoutError(f"OpenAI file upload for `{file_id}` is not ready yet")
|
|
112
|
+
message = payload.get("error_message") or "upload finalization returned an error"
|
|
113
|
+
raise RuntimeError(f"OpenAI file upload for `{file_id}` failed: {message}")
|
|
114
|
+
|
|
115
|
+
def _request_timeout(self, timeout: Any) -> Any:
|
|
116
|
+
return self._client._timeout if not _is_given(timeout) else timeout
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import time
|
|
5
6
|
from typing import Any, TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from .._models import Model, SyncPage
|
|
@@ -10,25 +11,38 @@ from .._utils import _UNSET
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from .._client import CodexClient
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
# This is the upstream Codex CLI protocol/client version expected by the
|
|
15
|
+
# ChatGPT backend, not the PyPI package version.
|
|
16
|
+
CLIENT_VERSION = "0.130.0"
|
|
17
|
+
_CACHE_TTL = 300
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
class Models:
|
|
17
21
|
def __init__(self, client: CodexClient) -> None:
|
|
18
22
|
self._client = client
|
|
23
|
+
self._cache: SyncPage | None = None
|
|
24
|
+
self._cache_fetched_at = 0.0
|
|
19
25
|
|
|
20
26
|
def list(
|
|
21
27
|
self,
|
|
22
28
|
*,
|
|
29
|
+
force_refresh: bool = False,
|
|
23
30
|
extra_headers: Any = None,
|
|
24
31
|
extra_query: Any = None,
|
|
25
32
|
extra_body: Any = None,
|
|
26
33
|
timeout: Any = _UNSET,
|
|
27
34
|
) -> SyncPage:
|
|
35
|
+
if not force_refresh and self._cache is not None:
|
|
36
|
+
if time.time() - self._cache_fetched_at <= _CACHE_TTL:
|
|
37
|
+
return self._cache
|
|
38
|
+
|
|
28
39
|
data = self._client._get("/models", params={"client_version": CLIENT_VERSION})
|
|
29
40
|
models = [_model_from_backend(item) for item in data.get("models", [])]
|
|
30
|
-
models.sort(key=lambda model: getattr(model, "priority", 0)
|
|
31
|
-
|
|
41
|
+
models.sort(key=lambda model: getattr(model, "priority", 0))
|
|
42
|
+
page = SyncPage(data=models)
|
|
43
|
+
self._cache = page
|
|
44
|
+
self._cache_fetched_at = time.time()
|
|
45
|
+
return page
|
|
32
46
|
|
|
33
47
|
def retrieve(
|
|
34
48
|
self,
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Realtime resources."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
4
|
from typing import Any, Optional, TYPE_CHECKING
|
|
7
5
|
|
|
8
6
|
from .._models import RealtimeCallResponse
|
|
@@ -51,17 +49,13 @@ class RealtimeCalls:
|
|
|
51
49
|
)
|
|
52
50
|
return RealtimeCallResponse(response)
|
|
53
51
|
|
|
54
|
-
files = [
|
|
55
|
-
("sdp", (None, sdp.encode("utf-8"), "application/sdp")),
|
|
56
|
-
(
|
|
57
|
-
"session",
|
|
58
|
-
(None, json.dumps(_jsonable(session)).encode("utf-8"), "application/json"),
|
|
59
|
-
),
|
|
60
|
-
]
|
|
61
52
|
response = self._client._post_raw(
|
|
62
53
|
"/realtime/calls",
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
body={
|
|
55
|
+
"sdp": sdp,
|
|
56
|
+
"session": _jsonable(session),
|
|
57
|
+
**(_jsonable(extra_body) if extra_body else {}),
|
|
58
|
+
},
|
|
65
59
|
headers={"Accept": "application/sdp", **(extra_headers or {})},
|
|
66
60
|
params=extra_query,
|
|
67
61
|
timeout=timeout,
|
{codex_backend_sdk-0.3.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/responses.py
RENAMED
|
@@ -115,14 +115,31 @@ class Responses:
|
|
|
115
115
|
input: list[dict[str, Any]],
|
|
116
116
|
model: Any = _UNSET,
|
|
117
117
|
instructions: Any = _UNSET,
|
|
118
|
+
tools: Any = _UNSET,
|
|
119
|
+
parallel_tool_calls: Any = _UNSET,
|
|
120
|
+
reasoning: Any = _UNSET,
|
|
121
|
+
service_tier: Any = _UNSET,
|
|
122
|
+
prompt_cache_key: Any = _UNSET,
|
|
123
|
+
text: Any = _UNSET,
|
|
118
124
|
) -> CompactedResponse:
|
|
125
|
+
normalized_tools = _normalize_tools(tools)
|
|
119
126
|
payload = {
|
|
120
127
|
"model": _default(model, self._client._defaults["model"]),
|
|
121
128
|
"instructions": _default(instructions, self._client._defaults["instructions"]) or "",
|
|
122
129
|
"input": [_normalize_input_item(item) for item in input],
|
|
123
|
-
"tools":
|
|
124
|
-
"parallel_tool_calls":
|
|
130
|
+
"tools": normalized_tools,
|
|
131
|
+
"parallel_tool_calls": (
|
|
132
|
+
bool(_default(parallel_tool_calls, False)) if normalized_tools else False
|
|
133
|
+
),
|
|
125
134
|
}
|
|
135
|
+
if _is_given(reasoning) and reasoning is not None:
|
|
136
|
+
payload["reasoning"] = _normalize_reasoning(reasoning)
|
|
137
|
+
if _is_given(service_tier) and service_tier is not None:
|
|
138
|
+
payload["service_tier"] = service_tier
|
|
139
|
+
if _is_given(prompt_cache_key) and prompt_cache_key is not None:
|
|
140
|
+
payload["prompt_cache_key"] = prompt_cache_key
|
|
141
|
+
if _is_given(text) and text is not None:
|
|
142
|
+
payload["text"] = _normalize_text(text)
|
|
126
143
|
data = self._client._post("/responses/compact", body=payload).json()
|
|
127
144
|
return CompactedResponse(id=data.get("id", ""), output=data.get("output", []))
|
|
128
145
|
|
|
@@ -43,7 +43,7 @@ List models available to this account.
|
|
|
43
43
|
**SDK method**: `client.models.list()` / `client.models.retrieve(model)`
|
|
44
44
|
|
|
45
45
|
**Query params**
|
|
46
|
-
- `client_version` —
|
|
46
|
+
- `client_version` — Codex CLI protocol/client version string (e.g. `"0.130.0"`).
|
|
47
47
|
|
|
48
48
|
**Response** — JSON `{ "models": [ ModelObject, … ] }`
|
|
49
49
|
|
|
@@ -193,7 +193,14 @@ Compact a long conversation into an encrypted summary the model can still read.
|
|
|
193
193
|
```json
|
|
194
194
|
{
|
|
195
195
|
"model": "gpt-5.4",
|
|
196
|
-
"input": [ /* full conversation history */ ]
|
|
196
|
+
"input": [ /* full conversation history */ ],
|
|
197
|
+
"instructions": "Compact the conversation.",
|
|
198
|
+
"tools": [],
|
|
199
|
+
"parallel_tool_calls": false,
|
|
200
|
+
"reasoning": { "effort": "medium" },
|
|
201
|
+
"service_tier": "priority",
|
|
202
|
+
"prompt_cache_key": "cache-key",
|
|
203
|
+
"text": { "verbosity": "low" }
|
|
197
204
|
}
|
|
198
205
|
```
|
|
199
206
|
|
|
@@ -219,7 +226,27 @@ Compact a long conversation into an encrypted summary the model can still read.
|
|
|
219
226
|
|
|
220
227
|
Summarize traces into persistent memories.
|
|
221
228
|
|
|
222
|
-
**
|
|
229
|
+
**SDK method**: `client.codex.memories.trace_summarize(...)`
|
|
230
|
+
|
|
231
|
+
**Status**: Supported as a raw helper. May return `403 Forbidden` depending on
|
|
232
|
+
plan/account capabilities.
|
|
233
|
+
|
|
234
|
+
**Request body**:
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"model": "gpt-5.4",
|
|
238
|
+
"traces": [
|
|
239
|
+
{
|
|
240
|
+
"id": "trace_1",
|
|
241
|
+
"metadata": { "source_path": "memory.jsonl" },
|
|
242
|
+
"items": [ /* normalized trace items */ ]
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
"reasoning": { "effort": "low" }
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Response** — raw JSON, typically `{ "output": [...] }`.
|
|
223
250
|
|
|
224
251
|
---
|
|
225
252
|
|
|
@@ -342,6 +369,8 @@ Known `rate_limit_reached_type.type` values: `rate_limit_reached`, `workspace_ow
|
|
|
342
369
|
|
|
343
370
|
Fetch managed requirements/config for this account (plan-gated settings).
|
|
344
371
|
|
|
372
|
+
**SDK method**: `client.codex.config.requirements()`
|
|
373
|
+
|
|
345
374
|
**Response** — JSON config blob; schema defined in `codex-rs/cloud-requirements`.
|
|
346
375
|
|
|
347
376
|
---
|
|
@@ -404,6 +433,27 @@ Remote control / agent-as-a-service websocket. Enrollment via:
|
|
|
404
433
|
|
|
405
434
|
---
|
|
406
435
|
|
|
436
|
+
## ChatGPT File Uploads
|
|
437
|
+
|
|
438
|
+
### `POST /backend-api/files`
|
|
439
|
+
|
|
440
|
+
Create file upload metadata for Codex Apps/MCP file parameters.
|
|
441
|
+
|
|
442
|
+
**SDK method**: `client.files.upload(path)`
|
|
443
|
+
|
|
444
|
+
The official flow is:
|
|
445
|
+
|
|
446
|
+
1. `POST /backend-api/files` with `file_name`, `file_size`, and
|
|
447
|
+
`use_case: "codex"`.
|
|
448
|
+
2. `PUT` the file bytes to the returned signed `upload_url`.
|
|
449
|
+
3. `POST /backend-api/files/{file_id}/uploaded` until the backend returns
|
|
450
|
+
`status: "success"`.
|
|
451
|
+
|
|
452
|
+
The SDK returns an `UploadedFile` object with `file_id`, canonical
|
|
453
|
+
`sediment://...` URI, download URL, file name, size, MIME type, and local path.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
407
457
|
## ChatGPT Account Data
|
|
408
458
|
|
|
409
459
|
These endpoints are under `https://chatgpt.com/backend-api`, not
|
|
@@ -6,11 +6,14 @@ class FakeCodexClient(OpenAI):
|
|
|
6
6
|
super().__init__(model="gpt-test")
|
|
7
7
|
self.wham_gets = []
|
|
8
8
|
self.chatgpt_gets = []
|
|
9
|
+
self.posts = []
|
|
9
10
|
|
|
10
11
|
def _get_wham(self, path, *, params=None):
|
|
11
12
|
self.wham_gets.append((path, params))
|
|
12
13
|
if path == "/wham/usage":
|
|
13
14
|
return {"rate_limit": {"allowed": True}}
|
|
15
|
+
if path == "/wham/config/requirements":
|
|
16
|
+
return {"requirements": [{"name": "network"}]}
|
|
14
17
|
if path == "/wham/tasks/list":
|
|
15
18
|
return {"items": [{"id": "task_1", "title": "Task"}], "cursor": None}
|
|
16
19
|
if path == "/wham/tasks/task_1":
|
|
@@ -35,6 +38,20 @@ class FakeCodexClient(OpenAI):
|
|
|
35
38
|
return {"object": "user_system_message_detail", "enabled": True}
|
|
36
39
|
raise AssertionError(f"Unexpected path: {path}")
|
|
37
40
|
|
|
41
|
+
def _post(self, path, *, body, stream=False):
|
|
42
|
+
self.posts.append((path, body, stream))
|
|
43
|
+
if path == "/memories/trace_summarize":
|
|
44
|
+
return FakeJSONResponse({"output": [{"memory_summary": "summary"}]})
|
|
45
|
+
raise AssertionError(f"Unexpected Codex post path: {path}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FakeJSONResponse:
|
|
49
|
+
def __init__(self, payload):
|
|
50
|
+
self._payload = payload
|
|
51
|
+
|
|
52
|
+
def json(self):
|
|
53
|
+
return self._payload
|
|
54
|
+
|
|
38
55
|
|
|
39
56
|
def test_codex_usage_still_calls_wham_usage():
|
|
40
57
|
client = FakeCodexClient()
|
|
@@ -54,6 +71,41 @@ def test_codex_memories_list_returns_raw_chatgpt_payload():
|
|
|
54
71
|
assert client.chatgpt_gets == ["/memories"]
|
|
55
72
|
|
|
56
73
|
|
|
74
|
+
def test_codex_memories_trace_summarize_posts_codex_payload():
|
|
75
|
+
client = FakeCodexClient()
|
|
76
|
+
|
|
77
|
+
response = client.codex.memories.trace_summarize(
|
|
78
|
+
model="gpt-test",
|
|
79
|
+
traces=[
|
|
80
|
+
{
|
|
81
|
+
"id": "trace_1",
|
|
82
|
+
"metadata": {"source_path": "/tmp/memory.jsonl"},
|
|
83
|
+
"items": [{"type": "message", "content": "remember me"}],
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
reasoning={"effort": "low"},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
assert response == {"output": [{"memory_summary": "summary"}]}
|
|
90
|
+
assert client.posts == [
|
|
91
|
+
(
|
|
92
|
+
"/memories/trace_summarize",
|
|
93
|
+
{
|
|
94
|
+
"model": "gpt-test",
|
|
95
|
+
"traces": [
|
|
96
|
+
{
|
|
97
|
+
"id": "trace_1",
|
|
98
|
+
"metadata": {"source_path": "/tmp/memory.jsonl"},
|
|
99
|
+
"items": [{"type": "message", "content": "remember me"}],
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"reasoning": {"effort": "low"},
|
|
103
|
+
},
|
|
104
|
+
False,
|
|
105
|
+
)
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
57
109
|
def test_codex_user_system_messages_retrieve_returns_raw_chatgpt_payload():
|
|
58
110
|
client = FakeCodexClient()
|
|
59
111
|
|
|
@@ -110,3 +162,12 @@ def test_codex_environments_list_returns_raw_payload():
|
|
|
110
162
|
|
|
111
163
|
assert environments == [{"id": "env_1", "label": "default"}]
|
|
112
164
|
assert client.wham_gets == [("/wham/environments", None)]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_codex_config_requirements_returns_raw_payload():
|
|
168
|
+
client = FakeCodexClient()
|
|
169
|
+
|
|
170
|
+
requirements = client.codex.config.requirements()
|
|
171
|
+
|
|
172
|
+
assert requirements == {"requirements": [{"name": "network"}]}
|
|
173
|
+
assert client.wham_gets == [("/wham/config/requirements", None)]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from codex_backend_sdk import OpenAI, UploadedFile
|
|
4
|
+
from codex_backend_sdk.storage import TokenStore
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FakePutResponse:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.raised = False
|
|
10
|
+
|
|
11
|
+
def raise_for_status(self):
|
|
12
|
+
self.raised = True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FakeFilesClient(OpenAI):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__(model="gpt-test")
|
|
18
|
+
self.chatgpt_posts = []
|
|
19
|
+
self._set_store(TokenStore(
|
|
20
|
+
access_token="chatgpt-token",
|
|
21
|
+
refresh_token="refresh-token",
|
|
22
|
+
id_token_raw="id-token",
|
|
23
|
+
account_id="account-id",
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
def _post_chatgpt(self, path, *, body, timeout=None):
|
|
27
|
+
self.chatgpt_posts.append((path, body, timeout))
|
|
28
|
+
if path == "/files":
|
|
29
|
+
return {"file_id": "file_123", "upload_url": "https://upload.example/file_123"}
|
|
30
|
+
if path == "/files/file_123/uploaded":
|
|
31
|
+
return {
|
|
32
|
+
"status": "success",
|
|
33
|
+
"download_url": "https://download.example/file_123",
|
|
34
|
+
"file_name": "report.txt",
|
|
35
|
+
"mime_type": "text/plain",
|
|
36
|
+
}
|
|
37
|
+
raise AssertionError(f"Unexpected ChatGPT post path: {path}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_files_upload_uses_chatgpt_file_flow(tmp_path, monkeypatch):
|
|
41
|
+
client = FakeFilesClient()
|
|
42
|
+
file_path = tmp_path / "report.txt"
|
|
43
|
+
file_path.write_text("hello")
|
|
44
|
+
put_calls = []
|
|
45
|
+
|
|
46
|
+
def fake_put(url, *, data, headers, timeout):
|
|
47
|
+
put_calls.append((url, data.read(), headers, timeout))
|
|
48
|
+
return FakePutResponse()
|
|
49
|
+
|
|
50
|
+
monkeypatch.setattr("codex_backend_sdk.resources.files.requests.put", fake_put)
|
|
51
|
+
|
|
52
|
+
uploaded = client.files.upload(file_path)
|
|
53
|
+
|
|
54
|
+
assert isinstance(uploaded, UploadedFile)
|
|
55
|
+
assert uploaded.file_id == "file_123"
|
|
56
|
+
assert uploaded.uri == "sediment://file_123"
|
|
57
|
+
assert uploaded.download_url == "https://download.example/file_123"
|
|
58
|
+
assert uploaded.file_name == "report.txt"
|
|
59
|
+
assert uploaded.file_size_bytes == 5
|
|
60
|
+
assert uploaded.mime_type == "text/plain"
|
|
61
|
+
assert uploaded.path == str(file_path)
|
|
62
|
+
assert client.chatgpt_posts[0][:2] == (
|
|
63
|
+
"/files",
|
|
64
|
+
{"file_name": "report.txt", "file_size": 5, "use_case": "codex"},
|
|
65
|
+
)
|
|
66
|
+
assert client.chatgpt_posts[1][:2] == ("/files/file_123/uploaded", {})
|
|
67
|
+
assert put_calls == [
|
|
68
|
+
(
|
|
69
|
+
"https://upload.example/file_123",
|
|
70
|
+
b"hello",
|
|
71
|
+
{"x-ms-blob-type": "BlockBlob", "Content-Length": "5"},
|
|
72
|
+
120,
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_files_upload_rejects_missing_path():
|
|
78
|
+
client = FakeFilesClient()
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
client.files.upload(Path("/does/not/exist"))
|
|
82
|
+
except FileNotFoundError as exc:
|
|
83
|
+
assert "does not exist" in str(exc)
|
|
84
|
+
else:
|
|
85
|
+
raise AssertionError("missing path should fail before network calls")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_files_upload_retries_finalize_payload(tmp_path, monkeypatch):
|
|
89
|
+
class RetryFilesClient(FakeFilesClient):
|
|
90
|
+
def __init__(self):
|
|
91
|
+
super().__init__()
|
|
92
|
+
self.finalize_calls = 0
|
|
93
|
+
|
|
94
|
+
def _post_chatgpt(self, path, *, body, timeout=None):
|
|
95
|
+
if path == "/files/file_123/uploaded":
|
|
96
|
+
self.finalize_calls += 1
|
|
97
|
+
if self.finalize_calls == 1:
|
|
98
|
+
return {"status": "retry"}
|
|
99
|
+
return super()._post_chatgpt(path, body=body, timeout=timeout)
|
|
100
|
+
|
|
101
|
+
client = RetryFilesClient()
|
|
102
|
+
file_path = tmp_path / "report.txt"
|
|
103
|
+
file_path.write_text("hello")
|
|
104
|
+
|
|
105
|
+
monkeypatch.setattr(
|
|
106
|
+
"codex_backend_sdk.resources.files.requests.put",
|
|
107
|
+
lambda *args, **kwargs: FakePutResponse(),
|
|
108
|
+
)
|
|
109
|
+
monkeypatch.setattr("codex_backend_sdk.resources.files.time.sleep", lambda delay: None)
|
|
110
|
+
|
|
111
|
+
uploaded = client.files.upload(file_path, finalize_retry_delay=0)
|
|
112
|
+
|
|
113
|
+
assert uploaded.file_id == "file_123"
|
|
114
|
+
assert client.finalize_calls == 2
|
|
@@ -51,7 +51,7 @@ def test_realtime_calls_create_posts_plain_sdp_like_official_sdk():
|
|
|
51
51
|
assert kwargs["headers"]["Content-Type"] == "application/sdp"
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def
|
|
54
|
+
def test_realtime_calls_create_posts_session_as_backend_json():
|
|
55
55
|
client = FakeRealtimeClient()
|
|
56
56
|
|
|
57
57
|
client.realtime.calls.create(
|
|
@@ -61,9 +61,11 @@ def test_realtime_calls_create_posts_session_as_multipart():
|
|
|
61
61
|
|
|
62
62
|
path, kwargs = client.raw_posts[0]
|
|
63
63
|
assert path == "/realtime/calls"
|
|
64
|
-
assert kwargs["
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
assert kwargs["body"] == {
|
|
65
|
+
"sdp": "offer-sdp",
|
|
66
|
+
"session": {"type": "realtime", "model": "gpt-realtime-1.5"},
|
|
67
|
+
}
|
|
68
|
+
assert kwargs["headers"]["Accept"] == "application/sdp"
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
def test_realtime_websocket_helpers_match_codex_agent_plugin_contract():
|
|
@@ -14,6 +14,14 @@ class FakeSSE:
|
|
|
14
14
|
yield b""
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class FakeJSONResponse:
|
|
18
|
+
def __init__(self, payload):
|
|
19
|
+
self._payload = payload
|
|
20
|
+
|
|
21
|
+
def json(self):
|
|
22
|
+
return self._payload
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
class FakeClient(OpenAI):
|
|
18
26
|
def __init__(self):
|
|
19
27
|
super().__init__(model="gpt-test")
|
|
@@ -22,6 +30,11 @@ class FakeClient(OpenAI):
|
|
|
22
30
|
|
|
23
31
|
def _post(self, path, *, body, stream=False):
|
|
24
32
|
self.posts.append((path, body, stream))
|
|
33
|
+
if path == "/responses/compact":
|
|
34
|
+
return FakeJSONResponse({
|
|
35
|
+
"id": "resp_123",
|
|
36
|
+
"output": [{"type": "message", "content": []}],
|
|
37
|
+
})
|
|
25
38
|
return FakeSSE([
|
|
26
39
|
'data: {"type":"response.content_part.delta","delta":{"text":"hel"}}',
|
|
27
40
|
"",
|
|
@@ -39,12 +52,20 @@ class FakeClient(OpenAI):
|
|
|
39
52
|
return {
|
|
40
53
|
"models": [
|
|
41
54
|
{
|
|
42
|
-
"slug": "gpt-
|
|
43
|
-
"display_name": "GPT
|
|
55
|
+
"slug": "gpt-lower-priority",
|
|
56
|
+
"display_name": "GPT Lower Priority",
|
|
44
57
|
"description": "Test model",
|
|
45
58
|
"context_window": 123,
|
|
46
59
|
"supported_in_api": True,
|
|
47
60
|
"priority": 7,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"slug": "gpt-test",
|
|
64
|
+
"display_name": "GPT Test",
|
|
65
|
+
"description": "Test model",
|
|
66
|
+
"context_window": 456,
|
|
67
|
+
"supported_in_api": True,
|
|
68
|
+
"priority": 1,
|
|
48
69
|
}
|
|
49
70
|
]
|
|
50
71
|
}
|
|
@@ -123,11 +144,58 @@ def test_models_resource_returns_iterable_page():
|
|
|
123
144
|
page = client.models.list()
|
|
124
145
|
model = page[0]
|
|
125
146
|
|
|
126
|
-
assert len(page) ==
|
|
127
|
-
assert list(page) == [model]
|
|
147
|
+
assert len(page) == 2
|
|
128
148
|
assert model.id == "gpt-test"
|
|
129
|
-
assert model.context_window ==
|
|
149
|
+
assert model.context_window == 456
|
|
130
150
|
assert client.models.retrieve("gpt-test").id == "gpt-test"
|
|
151
|
+
assert len(client.gets) == 1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_models_resource_can_force_refresh_cached_page():
|
|
155
|
+
client = FakeClient()
|
|
156
|
+
|
|
157
|
+
assert client.models.list() is client.models.list()
|
|
158
|
+
client.models.list(force_refresh=True)
|
|
159
|
+
|
|
160
|
+
assert len(client.gets) == 2
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_responses_compact_sends_shared_request_fields():
|
|
164
|
+
client = FakeClient()
|
|
165
|
+
|
|
166
|
+
compacted = client.responses.compact(
|
|
167
|
+
model="gpt-test",
|
|
168
|
+
input=[{"role": "user", "content": "Long context"}],
|
|
169
|
+
instructions="Compact this.",
|
|
170
|
+
tools=[{"type": "web_search"}],
|
|
171
|
+
parallel_tool_calls=True,
|
|
172
|
+
reasoning={"effort": "medium"},
|
|
173
|
+
service_tier="priority",
|
|
174
|
+
prompt_cache_key="cache-key",
|
|
175
|
+
text={"verbosity": "low"},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
assert compacted.id == "resp_123"
|
|
179
|
+
path, payload, stream = client.posts[0]
|
|
180
|
+
assert path == "/responses/compact"
|
|
181
|
+
assert stream is False
|
|
182
|
+
assert payload == {
|
|
183
|
+
"model": "gpt-test",
|
|
184
|
+
"instructions": "Compact this.",
|
|
185
|
+
"input": [
|
|
186
|
+
{
|
|
187
|
+
"type": "message",
|
|
188
|
+
"role": "user",
|
|
189
|
+
"content": [{"type": "input_text", "text": "Long context"}],
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
"tools": [{"type": "web_search_preview"}],
|
|
193
|
+
"parallel_tool_calls": True,
|
|
194
|
+
"reasoning": {"effort": "medium"},
|
|
195
|
+
"service_tier": "priority",
|
|
196
|
+
"prompt_cache_key": "cache-key",
|
|
197
|
+
"text": {"verbosity": "low"},
|
|
198
|
+
}
|
|
131
199
|
|
|
132
200
|
|
|
133
201
|
def test_responses_create_rejects_official_params_not_exposed_by_codex_backend():
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Probe OpenAI/Codex endpoints available to the current ChatGPT auth store.
|
|
3
|
-
|
|
4
|
-
The default mode is intentionally read-only: it sends GET/OPTIONS requests to
|
|
5
|
-
candidate endpoints and records status codes, content types, request IDs, and a
|
|
6
|
-
small body preview. It does not create resources.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import argparse
|
|
12
|
-
import json
|
|
13
|
-
import time
|
|
14
|
-
from dataclasses import asdict, dataclass
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any
|
|
17
|
-
from urllib.parse import urljoin
|
|
18
|
-
|
|
19
|
-
import requests
|
|
20
|
-
|
|
21
|
-
from codex_backend_sdk.storage import TokenStore, load_tokens
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
OPENAI_BASE_URL = "https://api.openai.com/v1"
|
|
25
|
-
CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
|
|
26
|
-
WHAM_BASE_URL = "https://chatgpt.com/backend-api"
|
|
27
|
-
ORIGINATOR = "codex_cli_rs"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass(frozen=True)
|
|
31
|
-
class Candidate:
|
|
32
|
-
name: str
|
|
33
|
-
base: str
|
|
34
|
-
method: str
|
|
35
|
-
path: str
|
|
36
|
-
auth: str
|
|
37
|
-
params: dict[str, Any] | None = None
|
|
38
|
-
headers: dict[str, str] | None = None
|
|
39
|
-
body: dict[str, Any] | None = None
|
|
40
|
-
mutating: bool = False
|
|
41
|
-
note: str = ""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@dataclass
|
|
45
|
-
class ProbeResult:
|
|
46
|
-
name: str
|
|
47
|
-
base: str
|
|
48
|
-
method: str
|
|
49
|
-
path: str
|
|
50
|
-
url: str
|
|
51
|
-
auth: str
|
|
52
|
-
status_code: int | None
|
|
53
|
-
category: str
|
|
54
|
-
elapsed_ms: int
|
|
55
|
-
content_type: str | None = None
|
|
56
|
-
request_id: str | None = None
|
|
57
|
-
allow: str | None = None
|
|
58
|
-
body_preview: Any = None
|
|
59
|
-
error: str | None = None
|
|
60
|
-
note: str = ""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def openai_candidates() -> list[Candidate]:
|
|
64
|
-
base_headers = {"OpenAI-Beta": "assistants=v2"}
|
|
65
|
-
return [
|
|
66
|
-
Candidate("models.list", OPENAI_BASE_URL, "GET", "/models", "openai"),
|
|
67
|
-
Candidate("files.list", OPENAI_BASE_URL, "GET", "/files", "openai", {"limit": 1}),
|
|
68
|
-
Candidate("vector_stores.list", OPENAI_BASE_URL, "GET", "/vector_stores", "openai", {"limit": 1}, base_headers),
|
|
69
|
-
Candidate("assistants.list", OPENAI_BASE_URL, "GET", "/assistants", "openai", {"limit": 1}, base_headers),
|
|
70
|
-
Candidate("threads.probe", OPENAI_BASE_URL, "OPTIONS", "/threads", "openai", headers=base_headers),
|
|
71
|
-
Candidate("responses.probe", OPENAI_BASE_URL, "OPTIONS", "/responses", "openai"),
|
|
72
|
-
Candidate("responses.compact.probe", OPENAI_BASE_URL, "OPTIONS", "/responses/compact", "openai"),
|
|
73
|
-
Candidate("realtime.calls.probe", OPENAI_BASE_URL, "OPTIONS", "/realtime/calls", "openai", headers={"OpenAI-Beta": "realtime=v1"}),
|
|
74
|
-
Candidate("batches.list", OPENAI_BASE_URL, "GET", "/batches", "openai", {"limit": 1}),
|
|
75
|
-
Candidate("uploads.probe", OPENAI_BASE_URL, "OPTIONS", "/uploads", "openai"),
|
|
76
|
-
Candidate("containers.list", OPENAI_BASE_URL, "GET", "/containers", "openai", {"limit": 1}),
|
|
77
|
-
Candidate("evals.list", OPENAI_BASE_URL, "GET", "/evals", "openai", {"limit": 1}),
|
|
78
|
-
Candidate("fine_tuning.jobs.list", OPENAI_BASE_URL, "GET", "/fine_tuning/jobs", "openai", {"limit": 1}),
|
|
79
|
-
Candidate("organization.costs", OPENAI_BASE_URL, "GET", "/organization/costs", "openai", {"limit": 1}),
|
|
80
|
-
Candidate("organization.usage.completions", OPENAI_BASE_URL, "GET", "/organization/usage/completions", "openai", {"limit": 1}),
|
|
81
|
-
Candidate("organization.usage.responses", OPENAI_BASE_URL, "GET", "/organization/usage/responses", "openai", {"limit": 1}),
|
|
82
|
-
Candidate("organization.usage.vector_stores", OPENAI_BASE_URL, "GET", "/organization/usage/vector_stores", "openai", {"limit": 1}),
|
|
83
|
-
Candidate("moderations.probe", OPENAI_BASE_URL, "OPTIONS", "/moderations", "openai"),
|
|
84
|
-
Candidate("images.probe", OPENAI_BASE_URL, "OPTIONS", "/images/generations", "openai"),
|
|
85
|
-
Candidate("audio.speech.probe", OPENAI_BASE_URL, "OPTIONS", "/audio/speech", "openai"),
|
|
86
|
-
Candidate("audio.transcriptions.probe", OPENAI_BASE_URL, "OPTIONS", "/audio/transcriptions", "openai"),
|
|
87
|
-
Candidate("chat.completions.probe", OPENAI_BASE_URL, "OPTIONS", "/chat/completions", "openai"),
|
|
88
|
-
Candidate("embeddings.probe", OPENAI_BASE_URL, "OPTIONS", "/embeddings", "openai"),
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def chatgpt_candidates() -> list[Candidate]:
|
|
93
|
-
return [
|
|
94
|
-
Candidate("codex.models", CODEX_BASE_URL, "GET", "/models", "chatgpt", {"client_version": "0.3.0"}),
|
|
95
|
-
Candidate("codex.responses.probe", CODEX_BASE_URL, "OPTIONS", "/responses", "chatgpt"),
|
|
96
|
-
Candidate("codex.responses.compact.probe", CODEX_BASE_URL, "OPTIONS", "/responses/compact", "chatgpt"),
|
|
97
|
-
Candidate("codex.realtime.calls.probe", CODEX_BASE_URL, "OPTIONS", "/realtime/calls", "chatgpt"),
|
|
98
|
-
Candidate("codex.memories.trace_summarize.probe", CODEX_BASE_URL, "OPTIONS", "/memories/trace_summarize", "chatgpt"),
|
|
99
|
-
Candidate("wham.usage", WHAM_BASE_URL, "GET", "/wham/usage", "chatgpt"),
|
|
100
|
-
Candidate("wham.config.requirements", WHAM_BASE_URL, "GET", "/wham/config/requirements", "chatgpt"),
|
|
101
|
-
Candidate("wham.tasks.list", WHAM_BASE_URL, "GET", "/wham/tasks/list", "chatgpt", {"limit": 1}),
|
|
102
|
-
Candidate("wham.remote.control.enroll.probe", WHAM_BASE_URL, "OPTIONS", "/wham/remote/control/server/enroll", "chatgpt"),
|
|
103
|
-
]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def build_headers(store: TokenStore, auth: str, extra: dict[str, str] | None) -> dict[str, str]:
|
|
107
|
-
if auth == "openai":
|
|
108
|
-
if not store.openai_api_key:
|
|
109
|
-
raise RuntimeError("No openai_api_key in auth store. Run authenticate(request_api_key=True).")
|
|
110
|
-
headers = {"Authorization": f"Bearer {store.openai_api_key}"}
|
|
111
|
-
elif auth == "chatgpt":
|
|
112
|
-
headers = {
|
|
113
|
-
"Authorization": f"Bearer {store.access_token}",
|
|
114
|
-
"originator": ORIGINATOR,
|
|
115
|
-
"OpenAI-Beta": "responses=experimental",
|
|
116
|
-
}
|
|
117
|
-
if store.account_id:
|
|
118
|
-
headers["ChatGPT-Account-ID"] = store.account_id
|
|
119
|
-
else:
|
|
120
|
-
raise ValueError(f"Unknown auth mode: {auth}")
|
|
121
|
-
|
|
122
|
-
headers.update(extra or {})
|
|
123
|
-
return headers
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def categorize(status_code: int | None, error: str | None = None) -> str:
|
|
127
|
-
if error is not None:
|
|
128
|
-
return "error"
|
|
129
|
-
if status_code is None:
|
|
130
|
-
return "error"
|
|
131
|
-
if 200 <= status_code < 300:
|
|
132
|
-
return "available"
|
|
133
|
-
if status_code == 401:
|
|
134
|
-
return "unauthenticated"
|
|
135
|
-
if status_code == 403:
|
|
136
|
-
return "forbidden_or_plan_gated"
|
|
137
|
-
if status_code == 404:
|
|
138
|
-
return "not_found_or_hidden"
|
|
139
|
-
if status_code == 405:
|
|
140
|
-
return "exists_method_not_allowed"
|
|
141
|
-
if status_code == 429:
|
|
142
|
-
return "rate_limited"
|
|
143
|
-
if 400 <= status_code < 500:
|
|
144
|
-
return "client_rejected"
|
|
145
|
-
if 500 <= status_code < 600:
|
|
146
|
-
return "server_error"
|
|
147
|
-
return "other"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def preview_body(response: requests.Response) -> Any:
|
|
151
|
-
text = response.text[:1000]
|
|
152
|
-
content_type = response.headers.get("content-type", "")
|
|
153
|
-
if "json" not in content_type:
|
|
154
|
-
return text
|
|
155
|
-
try:
|
|
156
|
-
data = response.json()
|
|
157
|
-
except ValueError:
|
|
158
|
-
return text
|
|
159
|
-
if isinstance(data, dict):
|
|
160
|
-
return redact(data)
|
|
161
|
-
return data
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def redact(value: Any) -> Any:
|
|
165
|
-
if isinstance(value, dict):
|
|
166
|
-
redacted: dict[str, Any] = {}
|
|
167
|
-
for key, item in value.items():
|
|
168
|
-
lowered = key.lower()
|
|
169
|
-
if any(secret in lowered for secret in ("token", "secret", "key", "email", "account_id", "user_id")):
|
|
170
|
-
redacted[key] = "[redacted]"
|
|
171
|
-
else:
|
|
172
|
-
redacted[key] = redact(item)
|
|
173
|
-
return redacted
|
|
174
|
-
if isinstance(value, list):
|
|
175
|
-
return [redact(item) for item in value]
|
|
176
|
-
return value
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def probe(candidate: Candidate, store: TokenStore, timeout: float) -> ProbeResult:
|
|
180
|
-
url = urljoin(candidate.base.rstrip("/") + "/", candidate.path.lstrip("/"))
|
|
181
|
-
start = time.monotonic()
|
|
182
|
-
try:
|
|
183
|
-
response = requests.request(
|
|
184
|
-
candidate.method,
|
|
185
|
-
url,
|
|
186
|
-
params=candidate.params,
|
|
187
|
-
json=candidate.body,
|
|
188
|
-
headers=build_headers(store, candidate.auth, candidate.headers),
|
|
189
|
-
timeout=timeout,
|
|
190
|
-
)
|
|
191
|
-
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
192
|
-
return ProbeResult(
|
|
193
|
-
name=candidate.name,
|
|
194
|
-
base=candidate.base,
|
|
195
|
-
method=candidate.method,
|
|
196
|
-
path=candidate.path,
|
|
197
|
-
url=response.url,
|
|
198
|
-
auth=candidate.auth,
|
|
199
|
-
status_code=response.status_code,
|
|
200
|
-
category=categorize(response.status_code),
|
|
201
|
-
elapsed_ms=elapsed_ms,
|
|
202
|
-
content_type=response.headers.get("content-type"),
|
|
203
|
-
request_id=response.headers.get("x-request-id") or response.headers.get("openai-processing-ms"),
|
|
204
|
-
allow=response.headers.get("allow"),
|
|
205
|
-
body_preview=preview_body(response),
|
|
206
|
-
note=candidate.note,
|
|
207
|
-
)
|
|
208
|
-
except Exception as exc:
|
|
209
|
-
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
210
|
-
return ProbeResult(
|
|
211
|
-
name=candidate.name,
|
|
212
|
-
base=candidate.base,
|
|
213
|
-
method=candidate.method,
|
|
214
|
-
path=candidate.path,
|
|
215
|
-
url=url,
|
|
216
|
-
auth=candidate.auth,
|
|
217
|
-
status_code=None,
|
|
218
|
-
category=categorize(None, str(exc)),
|
|
219
|
-
elapsed_ms=elapsed_ms,
|
|
220
|
-
error=f"{type(exc).__name__}: {exc}",
|
|
221
|
-
note=candidate.note,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def select_candidates(scope: str, include_mutating: bool) -> list[Candidate]:
|
|
226
|
-
candidates: list[Candidate] = []
|
|
227
|
-
if scope in {"all", "openai"}:
|
|
228
|
-
candidates.extend(openai_candidates())
|
|
229
|
-
if scope in {"all", "chatgpt"}:
|
|
230
|
-
candidates.extend(chatgpt_candidates())
|
|
231
|
-
if not include_mutating:
|
|
232
|
-
candidates = [candidate for candidate in candidates if not candidate.mutating]
|
|
233
|
-
return candidates
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def main() -> int:
|
|
237
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
238
|
-
parser.add_argument("--scope", choices=["all", "openai", "chatgpt"], default="all")
|
|
239
|
-
parser.add_argument("--output", type=Path, default=Path("endpoint-probe-results.json"))
|
|
240
|
-
parser.add_argument("--timeout", type=float, default=20)
|
|
241
|
-
parser.add_argument("--sleep", type=float, default=0.25, help="Delay between probes in seconds.")
|
|
242
|
-
parser.add_argument("--include-mutating", action="store_true", help="Include candidates marked mutating.")
|
|
243
|
-
args = parser.parse_args()
|
|
244
|
-
|
|
245
|
-
store = load_tokens()
|
|
246
|
-
if store is None:
|
|
247
|
-
raise SystemExit("No Codex auth store found. Run `python -c 'from codex_backend_sdk import OpenAI; OpenAI().authenticate()'` first.")
|
|
248
|
-
|
|
249
|
-
results: list[ProbeResult] = []
|
|
250
|
-
for candidate in select_candidates(args.scope, args.include_mutating):
|
|
251
|
-
result = probe(candidate, store, args.timeout)
|
|
252
|
-
results.append(result)
|
|
253
|
-
print(f"{result.category:25} {result.status_code or '-':>3} {candidate.method:7} {candidate.auth:7} {candidate.path} ({candidate.name})")
|
|
254
|
-
if args.sleep:
|
|
255
|
-
time.sleep(args.sleep)
|
|
256
|
-
|
|
257
|
-
payload = {
|
|
258
|
-
"generated_at": int(time.time()),
|
|
259
|
-
"scope": args.scope,
|
|
260
|
-
"read_only_default": not args.include_mutating,
|
|
261
|
-
"results": [asdict(result) for result in results],
|
|
262
|
-
}
|
|
263
|
-
args.output.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
264
|
-
print(f"\nWrote {args.output}")
|
|
265
|
-
return 0
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if __name__ == "__main__":
|
|
269
|
-
raise SystemExit(main())
|
|
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.0 → codex_backend_sdk-0.3.1}/codex_backend_sdk/resources/openai_oauth.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
|