codex-backend-sdk 0.3.8__tar.gz → 0.3.10__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.8 → codex_backend_sdk-0.3.10}/CHANGELOG.md +22 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/PKG-INFO +47 -1
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/README.md +46 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/__init__.py +11 -1
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_client.py +2 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_models.py +33 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/codex_client.py +10 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/codex.py +41 -1
- codex_backend_sdk-0.3.10/codex_backend_sdk/resources/images.py +101 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/openai_oauth.py +36 -1
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/docs/backend-api.md +38 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/pyproject.toml +1 -1
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_codex_resources.py +71 -1
- codex_backend_sdk-0.3.10/tests/test_images.py +132 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_openai_oauth_resources.py +20 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/uv.lock +1 -1
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/.gitignore +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/LICENSE +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_transport.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_utils.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/models.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/realtime.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/responses.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/examples/agent.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_account_info.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_client_retry.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_realtime_resource.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_responses_resource.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_structured_output.py +0 -0
- {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_tools.py +0 -0
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.10] - 2026-07-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added typed Codex rate-limit reset credit listing and explicit idempotent consumption through `client.codex.rate_limit_reset_credits`.
|
|
9
|
+
- Added ChatGPT-authenticated image generation and editing through `client.images.generate(...)` and `client.images.edit(...)`, returning typed base64 image data from the Codex backend.
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
- Documented that image generation uses the ChatGPT Codex backend rather than the separately billed Platform image endpoint, and that consuming reset credits mutates account quota state.
|
|
13
|
+
- Documented the verified Codex JSON image-edit contract using ordinary URLs or base64 data URLs.
|
|
14
|
+
|
|
15
|
+
### Tests
|
|
16
|
+
- Added behavioral coverage for credit payloads, redemption validation, image request construction, defaults, URL normalization, and typed responses; verified read-only credit listing plus real image generation and editing against the authenticated backend.
|
|
17
|
+
|
|
18
|
+
## [0.3.9] - 2026-07-17
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Detect anonymous audio buffers by their file signature before ChatGPT transcription uploads, correcting filenames and MIME types when callers provide misleading generic names such as `audio.mp3` for WAV data.
|
|
22
|
+
- Restored reliable `AIClient.audio_to_text(bytes)` integration with `/backend-api/transcribe` without requiring callers to understand multipart backend constraints.
|
|
23
|
+
|
|
24
|
+
### Tests
|
|
25
|
+
- Added regression coverage for WAV buffers carrying an incorrect `.mp3` name and verified the complete Codex Agent transcription path against the real ChatGPT backend.
|
|
26
|
+
|
|
5
27
|
## [0.3.8] - 2026-07-17
|
|
6
28
|
|
|
7
29
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-backend-sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Unofficial Python SDK for the ChatGPT Codex backend API
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -190,6 +190,10 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
|
|
|
190
190
|
| `wss://api.openai.com/v1/realtime?model=...` | `client.realtime_websocket_url(...)` / `client.realtime.websocket_headers(...)` | Voice v2 helpers; requires a Realtime API key obtained during OAuth or supplied by the auth store. |
|
|
191
191
|
| `POST /v1/embeddings` | `client.embeddings.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; usage is charged to the associated OpenAI Platform organization. |
|
|
192
192
|
| `POST /backend-api/transcribe` | `client.audio.transcriptions.create(...)` | Uses the authenticated ChatGPT backend for non-streaming batch transcription; no developer API key is required. |
|
|
193
|
+
| `POST /backend-api/codex/images/generations` | `client.images.generate(...)` | Generates images through the authenticated Codex backend and returns typed base64 image data. |
|
|
194
|
+
| `POST /backend-api/codex/images/edits` | `client.images.edit(...)` | Edits one or more URL/data-URL images through the authenticated Codex backend. |
|
|
195
|
+
| `GET /backend-api/codex/rate-limit-reset-credits` | `client.codex.rate_limit_reset_credits.list()` | Lists detailed reset credits available to the authenticated account. |
|
|
196
|
+
| `POST /backend-api/codex/rate-limit-reset-credits/consume` | `client.codex.rate_limit_reset_credits.consume(...)` | Consumes a reset credit using an idempotent redemption request ID. |
|
|
193
197
|
| `GET /backend-api/wham/usage` | `client.codex.usage()` | Codex/ChatGPT quota and rate-limit status. |
|
|
194
198
|
| `GET /backend-api/wham/config/requirements` | `client.codex.config.requirements()` | Raw managed requirements/config payload for the authenticated account. |
|
|
195
199
|
| `GET /backend-api/wham/tasks/list` | `client.codex.tasks.list(...)` | Raw Codex cloud task listing. |
|
|
@@ -395,6 +399,34 @@ with open("meeting.wav", "rb") as audio:
|
|
|
395
399
|
print(transcription.text)
|
|
396
400
|
```
|
|
397
401
|
|
|
402
|
+
### Image Generation
|
|
403
|
+
|
|
404
|
+
`client.images.generate(...)` uses the ChatGPT-authenticated Codex image backend,
|
|
405
|
+
not the separately billed OpenAI Platform image endpoint.
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
image = client.images.generate(
|
|
409
|
+
prompt="A cheerful blue robot holding a red flower",
|
|
410
|
+
model="gpt-image-2",
|
|
411
|
+
quality="auto",
|
|
412
|
+
size="auto",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
with open("robot.png", "wb") as output:
|
|
416
|
+
output.write(base64.b64decode(image.data[0].b64_json))
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The Codex contract supports `prompt`, `model`, `background`, `n`, `quality`,
|
|
420
|
+
and `size`. Editing accepts one or more ordinary URLs or data URLs:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
edited = client.images.edit(
|
|
424
|
+
images=["data:image/png;base64,..."],
|
|
425
|
+
prompt="Add a small red star in the center",
|
|
426
|
+
quality="low",
|
|
427
|
+
)
|
|
428
|
+
```
|
|
429
|
+
|
|
398
430
|
### Quota And Usage
|
|
399
431
|
|
|
400
432
|
`client.codex.usage()` calls the ChatGPT WHAM usage endpoint. It returns the raw
|
|
@@ -417,6 +449,20 @@ Typical fields include:
|
|
|
417
449
|
- `credits`
|
|
418
450
|
- `rate_limit_reached_type`
|
|
419
451
|
|
|
452
|
+
Detailed reset credits are available separately:
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
credits = client.codex.rate_limit_reset_credits.list()
|
|
456
|
+
for credit in credits.credits:
|
|
457
|
+
print(credit.id, credit.title, credit.expires_at)
|
|
458
|
+
|
|
459
|
+
# Consuming a credit is an account mutation. Use a unique idempotency key.
|
|
460
|
+
result = client.codex.rate_limit_reset_credits.consume(
|
|
461
|
+
redeem_request_id=str(uuid.uuid4()),
|
|
462
|
+
credit_id=credits.credits[0].id,
|
|
463
|
+
)
|
|
464
|
+
```
|
|
465
|
+
|
|
420
466
|
### Codex Cloud Tasks
|
|
421
467
|
|
|
422
468
|
The `client.codex.tasks` and `client.codex.environments` namespaces expose
|
|
@@ -164,6 +164,10 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
|
|
|
164
164
|
| `wss://api.openai.com/v1/realtime?model=...` | `client.realtime_websocket_url(...)` / `client.realtime.websocket_headers(...)` | Voice v2 helpers; requires a Realtime API key obtained during OAuth or supplied by the auth store. |
|
|
165
165
|
| `POST /v1/embeddings` | `client.embeddings.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; usage is charged to the associated OpenAI Platform organization. |
|
|
166
166
|
| `POST /backend-api/transcribe` | `client.audio.transcriptions.create(...)` | Uses the authenticated ChatGPT backend for non-streaming batch transcription; no developer API key is required. |
|
|
167
|
+
| `POST /backend-api/codex/images/generations` | `client.images.generate(...)` | Generates images through the authenticated Codex backend and returns typed base64 image data. |
|
|
168
|
+
| `POST /backend-api/codex/images/edits` | `client.images.edit(...)` | Edits one or more URL/data-URL images through the authenticated Codex backend. |
|
|
169
|
+
| `GET /backend-api/codex/rate-limit-reset-credits` | `client.codex.rate_limit_reset_credits.list()` | Lists detailed reset credits available to the authenticated account. |
|
|
170
|
+
| `POST /backend-api/codex/rate-limit-reset-credits/consume` | `client.codex.rate_limit_reset_credits.consume(...)` | Consumes a reset credit using an idempotent redemption request ID. |
|
|
167
171
|
| `GET /backend-api/wham/usage` | `client.codex.usage()` | Codex/ChatGPT quota and rate-limit status. |
|
|
168
172
|
| `GET /backend-api/wham/config/requirements` | `client.codex.config.requirements()` | Raw managed requirements/config payload for the authenticated account. |
|
|
169
173
|
| `GET /backend-api/wham/tasks/list` | `client.codex.tasks.list(...)` | Raw Codex cloud task listing. |
|
|
@@ -369,6 +373,34 @@ with open("meeting.wav", "rb") as audio:
|
|
|
369
373
|
print(transcription.text)
|
|
370
374
|
```
|
|
371
375
|
|
|
376
|
+
### Image Generation
|
|
377
|
+
|
|
378
|
+
`client.images.generate(...)` uses the ChatGPT-authenticated Codex image backend,
|
|
379
|
+
not the separately billed OpenAI Platform image endpoint.
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
image = client.images.generate(
|
|
383
|
+
prompt="A cheerful blue robot holding a red flower",
|
|
384
|
+
model="gpt-image-2",
|
|
385
|
+
quality="auto",
|
|
386
|
+
size="auto",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
with open("robot.png", "wb") as output:
|
|
390
|
+
output.write(base64.b64decode(image.data[0].b64_json))
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The Codex contract supports `prompt`, `model`, `background`, `n`, `quality`,
|
|
394
|
+
and `size`. Editing accepts one or more ordinary URLs or data URLs:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
edited = client.images.edit(
|
|
398
|
+
images=["data:image/png;base64,..."],
|
|
399
|
+
prompt="Add a small red star in the center",
|
|
400
|
+
quality="low",
|
|
401
|
+
)
|
|
402
|
+
```
|
|
403
|
+
|
|
372
404
|
### Quota And Usage
|
|
373
405
|
|
|
374
406
|
`client.codex.usage()` calls the ChatGPT WHAM usage endpoint. It returns the raw
|
|
@@ -391,6 +423,20 @@ Typical fields include:
|
|
|
391
423
|
- `credits`
|
|
392
424
|
- `rate_limit_reached_type`
|
|
393
425
|
|
|
426
|
+
Detailed reset credits are available separately:
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
credits = client.codex.rate_limit_reset_credits.list()
|
|
430
|
+
for credit in credits.credits:
|
|
431
|
+
print(credit.id, credit.title, credit.expires_at)
|
|
432
|
+
|
|
433
|
+
# Consuming a credit is an account mutation. Use a unique idempotency key.
|
|
434
|
+
result = client.codex.rate_limit_reset_credits.consume(
|
|
435
|
+
redeem_request_id=str(uuid.uuid4()),
|
|
436
|
+
credit_id=credits.credits[0].id,
|
|
437
|
+
)
|
|
438
|
+
```
|
|
439
|
+
|
|
394
440
|
### Codex Cloud Tasks
|
|
395
441
|
|
|
396
442
|
The `client.codex.tasks` and `client.codex.environments` namespaces expose
|
|
@@ -17,7 +17,7 @@ Quickstart:
|
|
|
17
17
|
print(response.output_text)
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
__version__ = "0.3.
|
|
20
|
+
__version__ = "0.3.10"
|
|
21
21
|
|
|
22
22
|
from .oauth import run_oauth_flow, refresh_access_token
|
|
23
23
|
from .storage import load_tokens, save_tokens, TokenStore
|
|
@@ -27,6 +27,11 @@ from .codex_client import (
|
|
|
27
27
|
CreateEmbeddingResponse,
|
|
28
28
|
Embedding,
|
|
29
29
|
EmbeddingUsage,
|
|
30
|
+
ImageData,
|
|
31
|
+
ImageResponse,
|
|
32
|
+
RateLimitResetCredit,
|
|
33
|
+
RateLimitResetCredits,
|
|
34
|
+
ConsumeRateLimitResetCreditResponse,
|
|
30
35
|
ReasoningEffort,
|
|
31
36
|
ReasoningSummary,
|
|
32
37
|
ServiceTier,
|
|
@@ -60,6 +65,11 @@ __all__ = [
|
|
|
60
65
|
"CreateEmbeddingResponse",
|
|
61
66
|
"Embedding",
|
|
62
67
|
"EmbeddingUsage",
|
|
68
|
+
"ImageData",
|
|
69
|
+
"ImageResponse",
|
|
70
|
+
"RateLimitResetCredit",
|
|
71
|
+
"RateLimitResetCredits",
|
|
72
|
+
"ConsumeRateLimitResetCreditResponse",
|
|
63
73
|
"MemorySummarizeOutput",
|
|
64
74
|
"MemorySummarizeResponse",
|
|
65
75
|
"Model",
|
|
@@ -40,6 +40,7 @@ class CodexClient:
|
|
|
40
40
|
from .resources.models import Models
|
|
41
41
|
from .resources.openai_oauth import Audio, Embeddings
|
|
42
42
|
from .resources.files import Files
|
|
43
|
+
from .resources.images import Images
|
|
43
44
|
from .resources.realtime import Realtime
|
|
44
45
|
from .resources.responses import Responses
|
|
45
46
|
|
|
@@ -59,6 +60,7 @@ class CodexClient:
|
|
|
59
60
|
self.realtime = Realtime(self)
|
|
60
61
|
self.embeddings = Embeddings(self)
|
|
61
62
|
self.audio = Audio(self)
|
|
63
|
+
self.images = Images(self)
|
|
62
64
|
self.files = Files(self)
|
|
63
65
|
self.codex = CodexResources(self)
|
|
64
66
|
|
|
@@ -225,6 +225,39 @@ class Transcription(CodexBaseModel):
|
|
|
225
225
|
text: str = ""
|
|
226
226
|
|
|
227
227
|
|
|
228
|
+
class ImageData(CodexBaseModel):
|
|
229
|
+
b64_json: str
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ImageResponse(CodexBaseModel):
|
|
233
|
+
created: int
|
|
234
|
+
data: list[ImageData] = Field(default_factory=list)
|
|
235
|
+
background: Optional[str] = None
|
|
236
|
+
quality: Optional[str] = None
|
|
237
|
+
size: Optional[str] = None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class RateLimitResetCredit(CodexBaseModel):
|
|
241
|
+
id: str
|
|
242
|
+
reset_type: str
|
|
243
|
+
status: str
|
|
244
|
+
granted_at: str
|
|
245
|
+
expires_at: Optional[str] = None
|
|
246
|
+
title: Optional[str] = None
|
|
247
|
+
description: Optional[str] = None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class RateLimitResetCredits(CodexBaseModel):
|
|
251
|
+
available_count: int = 0
|
|
252
|
+
total_earned_count: int = 0
|
|
253
|
+
credits: list[RateLimitResetCredit] = Field(default_factory=list)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class ConsumeRateLimitResetCreditResponse(CodexBaseModel):
|
|
257
|
+
code: str
|
|
258
|
+
windows_reset: int = 0
|
|
259
|
+
|
|
260
|
+
|
|
228
261
|
class RawMemoryMetadata(CodexBaseModel):
|
|
229
262
|
source_path: str
|
|
230
263
|
|
|
@@ -13,12 +13,17 @@ from ._models import (
|
|
|
13
13
|
CreateEmbeddingResponse,
|
|
14
14
|
Embedding,
|
|
15
15
|
EmbeddingUsage,
|
|
16
|
+
ImageData,
|
|
17
|
+
ImageResponse,
|
|
16
18
|
Model,
|
|
17
19
|
MemorySummarizeOutput,
|
|
18
20
|
MemorySummarizeResponse,
|
|
19
21
|
ParsedResponse,
|
|
20
22
|
RawMemory,
|
|
21
23
|
RawMemoryMetadata,
|
|
24
|
+
RateLimitResetCredit,
|
|
25
|
+
RateLimitResetCredits,
|
|
26
|
+
ConsumeRateLimitResetCreditResponse,
|
|
22
27
|
ReasoningEffort,
|
|
23
28
|
ReasoningSummary,
|
|
24
29
|
RealtimeCallResponse,
|
|
@@ -42,6 +47,8 @@ __all__ = [
|
|
|
42
47
|
"CreateEmbeddingResponse",
|
|
43
48
|
"Embedding",
|
|
44
49
|
"EmbeddingUsage",
|
|
50
|
+
"ImageData",
|
|
51
|
+
"ImageResponse",
|
|
45
52
|
"CompactedResponse",
|
|
46
53
|
"MemorySummarizeOutput",
|
|
47
54
|
"MemorySummarizeResponse",
|
|
@@ -50,6 +57,9 @@ __all__ = [
|
|
|
50
57
|
"ParsedResponse",
|
|
51
58
|
"RawMemory",
|
|
52
59
|
"RawMemoryMetadata",
|
|
60
|
+
"RateLimitResetCredit",
|
|
61
|
+
"RateLimitResetCredits",
|
|
62
|
+
"ConsumeRateLimitResetCreditResponse",
|
|
53
63
|
"ReasoningEffort",
|
|
54
64
|
"ReasoningSummary",
|
|
55
65
|
"Response",
|
|
@@ -4,7 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any, TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from .._models import
|
|
7
|
+
from .._models import (
|
|
8
|
+
ConsumeRateLimitResetCreditResponse,
|
|
9
|
+
MemorySummarizeResponse,
|
|
10
|
+
RateLimitResetCredits,
|
|
11
|
+
)
|
|
8
12
|
from .._utils import _jsonable
|
|
9
13
|
|
|
10
14
|
if TYPE_CHECKING:
|
|
@@ -20,12 +24,48 @@ class CodexResources:
|
|
|
20
24
|
self.tasks = CodexTasks(client)
|
|
21
25
|
self.environments = CodexEnvironments(client)
|
|
22
26
|
self.config = CodexConfig(client)
|
|
27
|
+
self.rate_limit_reset_credits = CodexRateLimitResetCredits(client)
|
|
23
28
|
self.user_system_messages = CodexUserSystemMessages(client)
|
|
24
29
|
|
|
25
30
|
def usage(self) -> dict[str, Any]:
|
|
26
31
|
return self._client._get_wham("/wham/usage")
|
|
27
32
|
|
|
28
33
|
|
|
34
|
+
class CodexRateLimitResetCredits:
|
|
35
|
+
"""Detailed Codex rate-limit reset credits for the authenticated account."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, client: CodexClient) -> None:
|
|
38
|
+
self._client = client
|
|
39
|
+
|
|
40
|
+
def list(self) -> RateLimitResetCredits:
|
|
41
|
+
return RateLimitResetCredits.model_validate(
|
|
42
|
+
self._client._get("/rate-limit-reset-credits")
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def consume(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
redeem_request_id: str,
|
|
49
|
+
credit_id: str | None = None,
|
|
50
|
+
) -> ConsumeRateLimitResetCreditResponse:
|
|
51
|
+
if not redeem_request_id:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"Expected a non-empty value for `redeem_request_id` "
|
|
54
|
+
f"but received {redeem_request_id!r}"
|
|
55
|
+
)
|
|
56
|
+
if credit_id == "":
|
|
57
|
+
raise ValueError("Expected `credit_id` to be non-empty when provided")
|
|
58
|
+
payload = {"redeem_request_id": redeem_request_id}
|
|
59
|
+
if credit_id is not None:
|
|
60
|
+
payload["credit_id"] = credit_id
|
|
61
|
+
return ConsumeRateLimitResetCreditResponse.model_validate(
|
|
62
|
+
self._client._post(
|
|
63
|
+
"/rate-limit-reset-credits/consume",
|
|
64
|
+
body=payload,
|
|
65
|
+
).json()
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
29
69
|
class CodexMemories:
|
|
30
70
|
"""ChatGPT memory data for the authenticated account."""
|
|
31
71
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""ChatGPT Codex image generation resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._models import ImageResponse
|
|
8
|
+
from .._utils import _UNSET, _add_given, _jsonable
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .._client import CodexClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Images:
|
|
15
|
+
"""Image generation through the authenticated Codex backend."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, client: CodexClient) -> None:
|
|
18
|
+
self._client = client
|
|
19
|
+
|
|
20
|
+
def generate(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
prompt: str,
|
|
24
|
+
model: str = "gpt-image-2",
|
|
25
|
+
background: Any = _UNSET,
|
|
26
|
+
n: Any = _UNSET,
|
|
27
|
+
quality: Any = _UNSET,
|
|
28
|
+
size: Any = _UNSET,
|
|
29
|
+
extra_body: Any = None,
|
|
30
|
+
timeout: Any = _UNSET,
|
|
31
|
+
) -> ImageResponse:
|
|
32
|
+
if not prompt:
|
|
33
|
+
raise ValueError(f"Expected a non-empty value for `prompt` but received {prompt!r}")
|
|
34
|
+
if not model:
|
|
35
|
+
raise ValueError(f"Expected a non-empty value for `model` but received {model!r}")
|
|
36
|
+
|
|
37
|
+
payload: dict[str, Any] = {"prompt": prompt, "model": model}
|
|
38
|
+
_add_given(payload, "background", background)
|
|
39
|
+
_add_given(payload, "n", n)
|
|
40
|
+
_add_given(payload, "quality", quality)
|
|
41
|
+
_add_given(payload, "size", size)
|
|
42
|
+
if extra_body:
|
|
43
|
+
payload.update(_jsonable(extra_body))
|
|
44
|
+
|
|
45
|
+
response = self._client._post_raw(
|
|
46
|
+
"/images/generations",
|
|
47
|
+
body=payload,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
)
|
|
50
|
+
return ImageResponse.model_validate(response.json())
|
|
51
|
+
|
|
52
|
+
def edit(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
images: list[str | dict[str, str]],
|
|
56
|
+
prompt: str,
|
|
57
|
+
model: str = "gpt-image-2",
|
|
58
|
+
background: Any = _UNSET,
|
|
59
|
+
n: Any = _UNSET,
|
|
60
|
+
quality: Any = _UNSET,
|
|
61
|
+
size: Any = _UNSET,
|
|
62
|
+
extra_body: Any = None,
|
|
63
|
+
timeout: Any = _UNSET,
|
|
64
|
+
) -> ImageResponse:
|
|
65
|
+
if not images:
|
|
66
|
+
raise ValueError("Expected at least one value for `images`")
|
|
67
|
+
if not prompt:
|
|
68
|
+
raise ValueError(f"Expected a non-empty value for `prompt` but received {prompt!r}")
|
|
69
|
+
if not model:
|
|
70
|
+
raise ValueError(f"Expected a non-empty value for `model` but received {model!r}")
|
|
71
|
+
|
|
72
|
+
normalized_images = []
|
|
73
|
+
for image in images:
|
|
74
|
+
if isinstance(image, str):
|
|
75
|
+
if not image:
|
|
76
|
+
raise ValueError("Expected every image URL to be non-empty")
|
|
77
|
+
normalized_images.append({"image_url": image})
|
|
78
|
+
continue
|
|
79
|
+
image_url = image.get("image_url")
|
|
80
|
+
if not image_url:
|
|
81
|
+
raise ValueError("Expected every image object to contain a non-empty `image_url`")
|
|
82
|
+
normalized_images.append({"image_url": image_url})
|
|
83
|
+
|
|
84
|
+
payload: dict[str, Any] = {
|
|
85
|
+
"images": normalized_images,
|
|
86
|
+
"prompt": prompt,
|
|
87
|
+
"model": model,
|
|
88
|
+
}
|
|
89
|
+
_add_given(payload, "background", background)
|
|
90
|
+
_add_given(payload, "n", n)
|
|
91
|
+
_add_given(payload, "quality", quality)
|
|
92
|
+
_add_given(payload, "size", size)
|
|
93
|
+
if extra_body:
|
|
94
|
+
payload.update(_jsonable(extra_body))
|
|
95
|
+
|
|
96
|
+
response = self._client._post_raw(
|
|
97
|
+
"/images/edits",
|
|
98
|
+
body=payload,
|
|
99
|
+
timeout=timeout,
|
|
100
|
+
)
|
|
101
|
+
return ImageResponse.model_validate(response.json())
|
{codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/openai_oauth.py
RENAMED
|
@@ -126,7 +126,7 @@ class AudioTranscriptions:
|
|
|
126
126
|
|
|
127
127
|
response = self._client._post_chatgpt_raw(
|
|
128
128
|
"/transcribe",
|
|
129
|
-
files={"file":
|
|
129
|
+
files={"file": _coerce_audio_file(file)},
|
|
130
130
|
data={key: _form_value(value) for key, value in data.items()},
|
|
131
131
|
headers=extra_headers,
|
|
132
132
|
params=extra_query,
|
|
@@ -136,3 +136,38 @@ class AudioTranscriptions:
|
|
|
136
136
|
if _is_given(response_format) and response_format == "text":
|
|
137
137
|
return transcription.text
|
|
138
138
|
return transcription
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _coerce_audio_file(file: Any) -> Any:
|
|
142
|
+
coerced = _coerce_file(file)
|
|
143
|
+
if isinstance(coerced, tuple):
|
|
144
|
+
return coerced
|
|
145
|
+
if not hasattr(coerced, "read"):
|
|
146
|
+
return coerced
|
|
147
|
+
|
|
148
|
+
position = coerced.tell() if hasattr(coerced, "tell") else None
|
|
149
|
+
header = coerced.read(12)
|
|
150
|
+
if position is not None and hasattr(coerced, "seek"):
|
|
151
|
+
coerced.seek(position)
|
|
152
|
+
|
|
153
|
+
detected = _audio_type(header)
|
|
154
|
+
if detected is None:
|
|
155
|
+
return coerced
|
|
156
|
+
extension, mime_type = detected
|
|
157
|
+
name = getattr(coerced, "name", "audio") or "audio"
|
|
158
|
+
stem = str(name).rsplit("/", 1)[-1].rsplit(".", 1)[0] or "audio"
|
|
159
|
+
return (f"{stem}.{extension}", coerced, mime_type)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _audio_type(header: bytes) -> tuple[str, str] | None:
|
|
163
|
+
if header.startswith(b"RIFF") and header[8:12] == b"WAVE":
|
|
164
|
+
return "wav", "audio/wav"
|
|
165
|
+
if header.startswith(b"ID3") or header[:2] in {b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"}:
|
|
166
|
+
return "mp3", "audio/mpeg"
|
|
167
|
+
if header.startswith(b"OggS"):
|
|
168
|
+
return "ogg", "audio/ogg"
|
|
169
|
+
if header.startswith(b"fLaC"):
|
|
170
|
+
return "flac", "audio/flac"
|
|
171
|
+
if header[4:8] == b"ftyp":
|
|
172
|
+
return "m4a", "audio/mp4"
|
|
173
|
+
return None
|
|
@@ -344,6 +344,26 @@ response options used by Codex Agent. Streaming, timestamps, speaker references,
|
|
|
344
344
|
chunking, SRT, and VTT are rejected locally rather than falling back to a
|
|
345
345
|
billable Platform endpoint.
|
|
346
346
|
|
|
347
|
+
### `POST /backend-api/codex/images/generations`
|
|
348
|
+
|
|
349
|
+
**SDK method**: `client.images.generate(...)`
|
|
350
|
+
|
|
351
|
+
**Status**: Supported through ChatGPT OAuth. Verified with `gpt-image-2`; the
|
|
352
|
+
response contains `created` and `data[].b64_json`, plus optional effective
|
|
353
|
+
`background`, `quality`, and `size` fields. Supported request fields mirror the
|
|
354
|
+
current Codex client: `prompt`, `model`, `background`, `n`, `quality`, and
|
|
355
|
+
`size`. This uses the Codex/ChatGPT backend rather than the OpenAI Platform
|
|
356
|
+
image endpoint.
|
|
357
|
+
|
|
358
|
+
### `POST /backend-api/codex/images/edits`
|
|
359
|
+
|
|
360
|
+
**SDK method**: `client.images.edit(...)`
|
|
361
|
+
|
|
362
|
+
**Status**: Supported through ChatGPT OAuth. Accepts one or more remote URLs or
|
|
363
|
+
`data:` URLs as `images[].image_url`, plus the same prompt/model/background/count/
|
|
364
|
+
quality/size controls as generation. Verified by generating a source image and
|
|
365
|
+
editing it through the authenticated backend.
|
|
366
|
+
|
|
347
367
|
### `POST /v1/audio/speech`
|
|
348
368
|
|
|
349
369
|
**Status**: Observed but not exposed. A malformed request reaches payload
|
|
@@ -389,6 +409,24 @@ Known `rate_limit_reached_type.type` values: `rate_limit_reached`, `workspace_ow
|
|
|
389
409
|
|
|
390
410
|
---
|
|
391
411
|
|
|
412
|
+
### `GET /backend-api/codex/rate-limit-reset-credits`
|
|
413
|
+
|
|
414
|
+
**SDK method**: `client.codex.rate_limit_reset_credits.list()`
|
|
415
|
+
|
|
416
|
+
Returns a typed detail payload containing `available_count`,
|
|
417
|
+
`total_earned_count`, and credit rows with IDs, reset type, status, grant and
|
|
418
|
+
expiry timestamps, title, and description. The equivalent WHAM path is also
|
|
419
|
+
currently accepted by ChatGPT, but the SDK follows the active Codex client
|
|
420
|
+
path.
|
|
421
|
+
|
|
422
|
+
### `POST /backend-api/codex/rate-limit-reset-credits/consume`
|
|
423
|
+
|
|
424
|
+
**SDK method**: `client.codex.rate_limit_reset_credits.consume(...)`
|
|
425
|
+
|
|
426
|
+
Consumes a reset credit. `redeem_request_id` is required as the idempotency key;
|
|
427
|
+
`credit_id` selects a specific available credit when supplied. This mutates
|
|
428
|
+
account quota state and is never called implicitly by the SDK.
|
|
429
|
+
|
|
392
430
|
### `GET /backend-api/wham/config/requirements`
|
|
393
431
|
|
|
394
432
|
Fetch managed requirements/config for this account (plan-gated settings).
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from codex_backend_sdk import (
|
|
4
|
+
ConsumeRateLimitResetCreditResponse,
|
|
5
|
+
MemorySummarizeResponse,
|
|
6
|
+
OpenAI,
|
|
7
|
+
RateLimitResetCredits,
|
|
8
|
+
RawMemory,
|
|
9
|
+
)
|
|
2
10
|
|
|
3
11
|
|
|
4
12
|
class FakeCodexClient(OpenAI):
|
|
@@ -26,6 +34,22 @@ class FakeCodexClient(OpenAI):
|
|
|
26
34
|
return [{"id": "env_1", "label": "default"}]
|
|
27
35
|
raise AssertionError(f"Unexpected WHAM path: {path}")
|
|
28
36
|
|
|
37
|
+
def _get(self, path, *, params=None):
|
|
38
|
+
if path == "/rate-limit-reset-credits":
|
|
39
|
+
return {
|
|
40
|
+
"available_count": 1,
|
|
41
|
+
"total_earned_count": 2,
|
|
42
|
+
"credits": [{
|
|
43
|
+
"id": "credit_1",
|
|
44
|
+
"reset_type": "codex_rate_limits",
|
|
45
|
+
"status": "available",
|
|
46
|
+
"granted_at": "2026-07-01T00:00:00Z",
|
|
47
|
+
"expires_at": "2026-08-01T00:00:00Z",
|
|
48
|
+
"title": "Full reset",
|
|
49
|
+
}],
|
|
50
|
+
}
|
|
51
|
+
raise AssertionError(f"Unexpected Codex get path: {path}")
|
|
52
|
+
|
|
29
53
|
def _get_chatgpt(self, path):
|
|
30
54
|
self.chatgpt_gets.append(path)
|
|
31
55
|
if path == "/memories":
|
|
@@ -44,6 +68,8 @@ class FakeCodexClient(OpenAI):
|
|
|
44
68
|
return FakeJSONResponse({
|
|
45
69
|
"output": [{"trace_summary": "raw", "memory_summary": "summary"}]
|
|
46
70
|
})
|
|
71
|
+
if path == "/rate-limit-reset-credits/consume":
|
|
72
|
+
return FakeJSONResponse({"code": "reset", "windows_reset": 2})
|
|
47
73
|
raise AssertionError(f"Unexpected Codex post path: {path}")
|
|
48
74
|
|
|
49
75
|
|
|
@@ -64,6 +90,50 @@ def test_codex_usage_still_calls_wham_usage():
|
|
|
64
90
|
assert client.wham_gets == [("/wham/usage", None)]
|
|
65
91
|
|
|
66
92
|
|
|
93
|
+
def test_codex_rate_limit_reset_credits_list_returns_typed_details():
|
|
94
|
+
client = FakeCodexClient()
|
|
95
|
+
|
|
96
|
+
result = client.codex.rate_limit_reset_credits.list()
|
|
97
|
+
|
|
98
|
+
assert isinstance(result, RateLimitResetCredits)
|
|
99
|
+
assert result.available_count == 1
|
|
100
|
+
assert result.total_earned_count == 2
|
|
101
|
+
assert result.credits[0].id == "credit_1"
|
|
102
|
+
assert result.credits[0].title == "Full reset"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_codex_rate_limit_reset_credits_consume_posts_idempotency_and_credit_ids():
|
|
106
|
+
client = FakeCodexClient()
|
|
107
|
+
|
|
108
|
+
result = client.codex.rate_limit_reset_credits.consume(
|
|
109
|
+
redeem_request_id="redeem_1",
|
|
110
|
+
credit_id="credit_1",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert isinstance(result, ConsumeRateLimitResetCreditResponse)
|
|
114
|
+
assert result.code == "reset"
|
|
115
|
+
assert result.windows_reset == 2
|
|
116
|
+
assert client.posts == [
|
|
117
|
+
(
|
|
118
|
+
"/rate-limit-reset-credits/consume",
|
|
119
|
+
{"redeem_request_id": "redeem_1", "credit_id": "credit_1"},
|
|
120
|
+
False,
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_codex_rate_limit_reset_credits_rejects_empty_identifiers():
|
|
126
|
+
client = FakeCodexClient()
|
|
127
|
+
|
|
128
|
+
with pytest.raises(ValueError, match="redeem_request_id"):
|
|
129
|
+
client.codex.rate_limit_reset_credits.consume(redeem_request_id="")
|
|
130
|
+
with pytest.raises(ValueError, match="credit_id"):
|
|
131
|
+
client.codex.rate_limit_reset_credits.consume(
|
|
132
|
+
redeem_request_id="redeem_1",
|
|
133
|
+
credit_id="",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
67
137
|
def test_codex_memories_list_returns_raw_chatgpt_payload():
|
|
68
138
|
client = FakeCodexClient()
|
|
69
139
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from codex_backend_sdk import ImageResponse, OpenAI
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeJSONResponse:
|
|
7
|
+
def __init__(self, payload):
|
|
8
|
+
self._payload = payload
|
|
9
|
+
|
|
10
|
+
def json(self):
|
|
11
|
+
return self._payload
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeImagesClient(OpenAI):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__(model="gpt-test")
|
|
17
|
+
self.posts = []
|
|
18
|
+
|
|
19
|
+
def _post_raw(self, path, **kwargs):
|
|
20
|
+
self.posts.append((path, kwargs))
|
|
21
|
+
return FakeJSONResponse({
|
|
22
|
+
"created": 1,
|
|
23
|
+
"data": [{"b64_json": "aW1hZ2U="}],
|
|
24
|
+
"background": "opaque",
|
|
25
|
+
"quality": "medium",
|
|
26
|
+
"size": "1024x1024",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_images_generate_posts_codex_payload_and_returns_typed_response():
|
|
31
|
+
client = FakeImagesClient()
|
|
32
|
+
|
|
33
|
+
response = client.images.generate(
|
|
34
|
+
prompt="a cheerful blue robot",
|
|
35
|
+
background="opaque",
|
|
36
|
+
n=1,
|
|
37
|
+
quality="medium",
|
|
38
|
+
size="1024x1024",
|
|
39
|
+
extra_body={"custom": {"enabled": True}},
|
|
40
|
+
timeout=180,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert isinstance(response, ImageResponse)
|
|
44
|
+
assert response.data[0].b64_json == "aW1hZ2U="
|
|
45
|
+
assert response.background == "opaque"
|
|
46
|
+
assert client.posts == [
|
|
47
|
+
(
|
|
48
|
+
"/images/generations",
|
|
49
|
+
{
|
|
50
|
+
"body": {
|
|
51
|
+
"prompt": "a cheerful blue robot",
|
|
52
|
+
"model": "gpt-image-2",
|
|
53
|
+
"background": "opaque",
|
|
54
|
+
"n": 1,
|
|
55
|
+
"quality": "medium",
|
|
56
|
+
"size": "1024x1024",
|
|
57
|
+
"custom": {"enabled": True},
|
|
58
|
+
},
|
|
59
|
+
"timeout": 180,
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_images_edit_normalizes_urls_and_posts_codex_payload():
|
|
66
|
+
client = FakeImagesClient()
|
|
67
|
+
|
|
68
|
+
response = client.images.edit(
|
|
69
|
+
images=[
|
|
70
|
+
"data:image/png;base64,aW1hZ2U=",
|
|
71
|
+
{"image_url": "https://example.test/reference.png"},
|
|
72
|
+
],
|
|
73
|
+
prompt="add a red star",
|
|
74
|
+
quality="low",
|
|
75
|
+
timeout=240,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert isinstance(response, ImageResponse)
|
|
79
|
+
assert client.posts == [
|
|
80
|
+
(
|
|
81
|
+
"/images/edits",
|
|
82
|
+
{
|
|
83
|
+
"body": {
|
|
84
|
+
"images": [
|
|
85
|
+
{"image_url": "data:image/png;base64,aW1hZ2U="},
|
|
86
|
+
{"image_url": "https://example.test/reference.png"},
|
|
87
|
+
],
|
|
88
|
+
"prompt": "add a red star",
|
|
89
|
+
"model": "gpt-image-2",
|
|
90
|
+
"quality": "low",
|
|
91
|
+
},
|
|
92
|
+
"timeout": 240,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
"images, match",
|
|
100
|
+
[
|
|
101
|
+
([], "at least one"),
|
|
102
|
+
([""], "non-empty"),
|
|
103
|
+
([{}], "image_url"),
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
def test_images_edit_rejects_invalid_images(images, match):
|
|
107
|
+
client = FakeImagesClient()
|
|
108
|
+
|
|
109
|
+
with pytest.raises(ValueError, match=match):
|
|
110
|
+
client.images.edit(images=images, prompt="add a star")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_images_generate_uses_minimal_default_payload():
|
|
114
|
+
client = FakeImagesClient()
|
|
115
|
+
|
|
116
|
+
client.images.generate(prompt="a red flower")
|
|
117
|
+
|
|
118
|
+
_, kwargs = client.posts[0]
|
|
119
|
+
assert kwargs["body"] == {
|
|
120
|
+
"prompt": "a red flower",
|
|
121
|
+
"model": "gpt-image-2",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize("field", ["prompt", "model"])
|
|
126
|
+
def test_images_generate_rejects_empty_required_fields(field):
|
|
127
|
+
client = FakeImagesClient()
|
|
128
|
+
arguments = {"prompt": "a red flower", "model": "gpt-image-2"}
|
|
129
|
+
arguments[field] = ""
|
|
130
|
+
|
|
131
|
+
with pytest.raises(ValueError, match=field):
|
|
132
|
+
client.images.generate(**arguments)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
from codex_backend_sdk import CreateEmbeddingResponse, OpenAI, Transcription
|
|
@@ -92,6 +94,24 @@ def test_audio_transcriptions_create_posts_to_chatgpt_backend():
|
|
|
92
94
|
assert headers["ChatGPT-Account-ID"] == "account-id"
|
|
93
95
|
|
|
94
96
|
|
|
97
|
+
def test_audio_transcriptions_detects_anonymous_audio_format():
|
|
98
|
+
client = FakeOpenAIClient()
|
|
99
|
+
audio = BytesIO(b"RIFF\x00\x00\x00\x00WAVEfmt ")
|
|
100
|
+
audio.name = "audio.mp3"
|
|
101
|
+
|
|
102
|
+
client.audio.transcriptions.create(
|
|
103
|
+
model="gpt-4o-mini-transcribe",
|
|
104
|
+
file=audio,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
_, kwargs, _ = client.chatgpt_raw_posts[0]
|
|
108
|
+
filename, uploaded, mime_type = kwargs["files"]["file"]
|
|
109
|
+
assert filename == "audio.wav"
|
|
110
|
+
assert uploaded is audio
|
|
111
|
+
assert mime_type == "audio/wav"
|
|
112
|
+
assert audio.tell() == 0
|
|
113
|
+
|
|
114
|
+
|
|
95
115
|
def test_audio_transcriptions_text_format_returns_string():
|
|
96
116
|
client = FakeOpenAIClient()
|
|
97
117
|
|
|
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.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/realtime.py
RENAMED
|
File without changes
|
{codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/responses.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|