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.
Files changed (42) hide show
  1. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/CHANGELOG.md +22 -0
  2. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/PKG-INFO +47 -1
  3. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/README.md +46 -0
  4. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/__init__.py +11 -1
  5. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_client.py +2 -0
  6. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_models.py +33 -0
  7. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/codex_client.py +10 -0
  8. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/codex.py +41 -1
  9. codex_backend_sdk-0.3.10/codex_backend_sdk/resources/images.py +101 -0
  10. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/openai_oauth.py +36 -1
  11. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/docs/backend-api.md +38 -0
  12. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/pyproject.toml +1 -1
  13. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_codex_resources.py +71 -1
  14. codex_backend_sdk-0.3.10/tests/test_images.py +132 -0
  15. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_openai_oauth_resources.py +20 -0
  16. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/uv.lock +1 -1
  17. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/.gitignore +0 -0
  18. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/LICENSE +0 -0
  19. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_streaming.py +0 -0
  20. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_transport.py +0 -0
  21. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/_utils.py +0 -0
  22. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/oauth.py +0 -0
  23. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/pkce.py +0 -0
  24. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/__init__.py +0 -0
  25. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
  26. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/files.py +0 -0
  27. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/models.py +0 -0
  28. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/realtime.py +0 -0
  29. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/resources/responses.py +0 -0
  30. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/codex_backend_sdk/storage.py +0 -0
  31. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/examples/agent.py +0 -0
  32. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/conftest.py +0 -0
  33. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_account_info.py +0 -0
  34. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_basic.py +0 -0
  35. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_client_retry.py +0 -0
  36. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_conversation.py +0 -0
  37. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_files_resource.py +0 -0
  38. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_realtime_resource.py +0 -0
  39. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_reasoning.py +0 -0
  40. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_responses_resource.py +0 -0
  41. {codex_backend_sdk-0.3.8 → codex_backend_sdk-0.3.10}/tests/test_structured_output.py +0 -0
  42. {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.8
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.8"
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 MemorySummarizeResponse
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())
@@ -126,7 +126,7 @@ class AudioTranscriptions:
126
126
 
127
127
  response = self._client._post_chatgpt_raw(
128
128
  "/transcribe",
129
- files={"file": _coerce_file(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).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-backend-sdk"
7
- version = "0.3.8"
7
+ version = "0.3.10"
8
8
  description = "Unofficial Python SDK for the ChatGPT Codex backend API"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,4 +1,12 @@
1
- from codex_backend_sdk import MemorySummarizeResponse, OpenAI, RawMemory
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
 
@@ -160,7 +160,7 @@ wheels = [
160
160
 
161
161
  [[package]]
162
162
  name = "codex-backend-sdk"
163
- version = "0.3.8"
163
+ version = "0.3.10"
164
164
  source = { editable = "." }
165
165
  dependencies = [
166
166
  { name = "pydantic" },