codex-backend-sdk 0.3.7__tar.gz → 0.3.8__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 (40) hide show
  1. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/CHANGELOG.md +13 -0
  2. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/PKG-INFO +3 -3
  3. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/README.md +2 -2
  4. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/__init__.py +1 -1
  5. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_client.py +21 -2
  6. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/openai_oauth.py +41 -29
  7. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/docs/backend-api.md +15 -7
  8. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/pyproject.toml +1 -1
  9. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_openai_oauth_resources.py +40 -6
  10. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/uv.lock +1 -1
  11. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/.gitignore +0 -0
  12. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/LICENSE +0 -0
  13. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_models.py +0 -0
  14. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_streaming.py +0 -0
  15. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_transport.py +0 -0
  16. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_utils.py +0 -0
  17. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/codex_client.py +0 -0
  18. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/oauth.py +0 -0
  19. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/pkce.py +0 -0
  20. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/__init__.py +0 -0
  21. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
  22. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/codex.py +0 -0
  23. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/files.py +0 -0
  24. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/models.py +0 -0
  25. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/realtime.py +0 -0
  26. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/responses.py +0 -0
  27. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/codex_backend_sdk/storage.py +0 -0
  28. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/examples/agent.py +0 -0
  29. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/conftest.py +0 -0
  30. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_account_info.py +0 -0
  31. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_basic.py +0 -0
  32. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_client_retry.py +0 -0
  33. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_codex_resources.py +0 -0
  34. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_conversation.py +0 -0
  35. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_files_resource.py +0 -0
  36. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_realtime_resource.py +0 -0
  37. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_reasoning.py +0 -0
  38. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_responses_resource.py +0 -0
  39. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_structured_output.py +0 -0
  40. {codex_backend_sdk-0.3.7 → codex_backend_sdk-0.3.8}/tests/test_tools.py +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.8] - 2026-07-17
6
+
7
+ ### Changed
8
+ - Routed `client.audio.transcriptions.create(...)` through the ChatGPT-native `/backend-api/transcribe` endpoint instead of the billable Platform `/v1/audio/transcriptions` endpoint.
9
+ - Preserved the OpenAI-shaped `json` and `text` response behavior used by Codex Agent while rejecting unsupported streaming, timestamp, speaker, chunking, SRT, and VTT options explicitly.
10
+ - Added a reusable raw ChatGPT multipart request helper to the client transport.
11
+
12
+ ### Documentation
13
+ - Clarified that embeddings still use the OpenAI Platform endpoint and its developer-account quota, while batch transcription now uses the authenticated ChatGPT backend.
14
+
15
+ ### Tests
16
+ - Added coverage for ChatGPT transcription routing, account authentication headers, text responses, and unsupported parameters.
17
+
5
18
  ## [0.3.7] - 2026-07-17
6
19
 
7
20
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-backend-sdk
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -188,8 +188,8 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
188
188
  | `GET /backend-api/codex/models` | `client.models.list()` / `client.models.retrieve(...)` | OpenAI-shaped model objects with Codex metadata preserved as extra fields. |
189
189
  | `POST /backend-api/codex/realtime/calls` | `client.realtime.calls.create(...)` | Experimental SDP call creation. The protocol is implemented by Codex, but ChatGPT routing is rollout-dependent and may return `404 Not Found`. |
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
- | `POST /v1/embeddings` | `client.embeddings.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; verified with `text-embedding-3-small`. |
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`. |
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
+ | `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
193
  | `GET /backend-api/wham/usage` | `client.codex.usage()` | Codex/ChatGPT quota and rate-limit status. |
194
194
  | `GET /backend-api/wham/config/requirements` | `client.codex.config.requirements()` | Raw managed requirements/config payload for the authenticated account. |
195
195
  | `GET /backend-api/wham/tasks/list` | `client.codex.tasks.list(...)` | Raw Codex cloud task listing. |
@@ -162,8 +162,8 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
162
162
  | `GET /backend-api/codex/models` | `client.models.list()` / `client.models.retrieve(...)` | OpenAI-shaped model objects with Codex metadata preserved as extra fields. |
163
163
  | `POST /backend-api/codex/realtime/calls` | `client.realtime.calls.create(...)` | Experimental SDP call creation. The protocol is implemented by Codex, but ChatGPT routing is rollout-dependent and may return `404 Not Found`. |
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
- | `POST /v1/embeddings` | `client.embeddings.create(...)` | Uses the Codex OAuth access token against `api.openai.com`; verified with `text-embedding-3-small`. |
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`. |
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
+ | `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
167
  | `GET /backend-api/wham/usage` | `client.codex.usage()` | Codex/ChatGPT quota and rate-limit status. |
168
168
  | `GET /backend-api/wham/config/requirements` | `client.codex.config.requirements()` | Raw managed requirements/config payload for the authenticated account. |
169
169
  | `GET /backend-api/wham/tasks/list` | `client.codex.tasks.list(...)` | Raw Codex cloud task listing. |
@@ -17,7 +17,7 @@ Quickstart:
17
17
  print(response.output_text)
18
18
  """
19
19
 
20
- __version__ = "0.3.7"
20
+ __version__ = "0.3.8"
21
21
 
22
22
  from .oauth import run_oauth_flow, refresh_access_token
23
23
  from .storage import load_tokens, save_tokens, TokenStore
@@ -295,14 +295,33 @@ class CodexClient:
295
295
  body: dict[str, Any],
296
296
  timeout: Any = _UNSET,
297
297
  ) -> dict[str, Any]:
298
+ response = self._post_chatgpt_raw(path, body=body, timeout=timeout)
299
+ return response.json()
300
+
301
+ def _post_chatgpt_raw(
302
+ self,
303
+ path: str,
304
+ *,
305
+ body: Optional[dict[str, Any]] = None,
306
+ files: Any = None,
307
+ data: Any = None,
308
+ headers: Optional[dict[str, str]] = None,
309
+ params: Optional[dict[str, Any]] = None,
310
+ timeout: Any = _UNSET,
311
+ ) -> requests.Response:
298
312
  self._ensure_auth()
299
313
  response = self._request_with_retries(
300
314
  "POST",
301
315
  f"{CHATGPT_BASE_URL}{path}",
302
- json=body,
316
+ json=body if files is None and data is None else None,
317
+ files=files,
318
+ data=data,
319
+ headers=headers,
320
+ params=params,
303
321
  timeout=self._timeout if not _is_given(timeout) else timeout,
304
322
  )
305
- return response.json()
323
+ response.raise_for_status()
324
+ return response
306
325
 
307
326
  def _request_with_retries(self, method: str, url: str, **kwargs: Any) -> requests.Response:
308
327
  use_session = kwargs.pop("_use_session", True)
@@ -1,15 +1,19 @@
1
- """OpenAI v1 resources that accept the Codex OAuth access token."""
1
+ """OpenAI-shaped resources authenticated through the Codex ChatGPT session."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Iterator
6
5
  from typing import Any, Optional, TYPE_CHECKING
7
6
 
8
- import requests
9
-
10
- from .._models import CreateEmbeddingResponse, ResponseStreamEvent, Transcription
11
- from .._streaming import stream_response_events
12
- from .._utils import _UNSET, _add_given, _coerce_file, _form_value, _is_given, _jsonable
7
+ from .._models import CreateEmbeddingResponse, Transcription
8
+ from .._utils import (
9
+ _UNSET,
10
+ CodexBackendUnsupportedParameterError,
11
+ _add_given,
12
+ _coerce_file,
13
+ _form_value,
14
+ _is_given,
15
+ _jsonable,
16
+ )
13
17
 
14
18
  if TYPE_CHECKING:
15
19
  from .._client import CodexClient
@@ -85,42 +89,50 @@ class AudioTranscriptions:
85
89
  extra_query: Optional[dict[str, Any]] = None,
86
90
  extra_body: Any = None,
87
91
  timeout: Any = _UNSET,
88
- ) -> str | Transcription | Iterator[ResponseStreamEvent]:
92
+ ) -> str | Transcription:
89
93
  if not model:
90
94
  raise ValueError(f"Expected a non-empty value for `model` but received {model!r}")
91
95
 
96
+ if _is_given(response_format) and response_format not in {None, "json", "text"}:
97
+ raise CodexBackendUnsupportedParameterError(
98
+ "The ChatGPT transcription backend supports only `json` and `text` response formats."
99
+ )
100
+
101
+ unsupported = {
102
+ "chunking_strategy": chunking_strategy,
103
+ "include": include,
104
+ "known_speaker_names": known_speaker_names,
105
+ "known_speaker_references": known_speaker_references,
106
+ "stream": stream,
107
+ "timestamp_granularities": timestamp_granularities,
108
+ }
109
+ given_unsupported = [
110
+ name for name, value in unsupported.items()
111
+ if _is_given(value) and value not in {None, False}
112
+ ]
113
+ if given_unsupported:
114
+ raise CodexBackendUnsupportedParameterError(
115
+ "The ChatGPT transcription backend does not support: "
116
+ + ", ".join(sorted(given_unsupported))
117
+ )
118
+
92
119
  data = {"model": model}
93
- _add_given(data, "chunking_strategy", chunking_strategy)
94
- _add_given(data, "include", include)
95
- _add_given(data, "known_speaker_names", known_speaker_names)
96
- _add_given(data, "known_speaker_references", known_speaker_references)
97
120
  _add_given(data, "language", language)
98
121
  _add_given(data, "prompt", prompt)
99
122
  _add_given(data, "response_format", response_format)
100
- _add_given(data, "stream", stream)
101
123
  _add_given(data, "temperature", temperature)
102
- _add_given(data, "timestamp_granularities", timestamp_granularities)
103
124
  if extra_body:
104
125
  data.update(_jsonable(extra_body))
105
126
 
106
- stream_enabled = bool(stream) if _is_given(stream) else False
107
- response = self._client._post_openai_raw(
108
- "/audio/transcriptions",
127
+ response = self._client._post_chatgpt_raw(
128
+ "/transcribe",
109
129
  files={"file": _coerce_file(file)},
110
130
  data={key: _form_value(value) for key, value in data.items()},
111
131
  headers=extra_headers,
112
132
  params=extra_query,
113
133
  timeout=timeout,
114
- stream=stream_enabled,
115
134
  )
116
- if stream_enabled:
117
- return stream_response_events(response)
118
- if _wants_text_response(response_format, response):
119
- return response.text
120
- return Transcription.model_validate(response.json())
121
-
122
-
123
- def _wants_text_response(response_format: Any, response: requests.Response) -> bool:
124
- if _is_given(response_format) and response_format in {"text", "srt", "vtt"}:
125
- return True
126
- return "json" not in response.headers.get("content-type", "")
135
+ transcription = Transcription.model_validate(response.json())
136
+ if _is_given(response_format) and response_format == "text":
137
+ return transcription.text
138
+ return transcription
@@ -306,11 +306,12 @@ continues to work but Voice v2 requires a separately provisioned API key.
306
306
 
307
307
  ---
308
308
 
309
- ## OpenAI API Endpoints With Codex OAuth
309
+ ## Embeddings and Transcription
310
310
 
311
- These endpoints live under `https://api.openai.com/v1`, not the ChatGPT backend,
312
- but they work with the same Codex OAuth access token stored in
313
- `~/.codex/auth.json`.
311
+ These OpenAI-shaped resources deliberately use different upstreams. Embeddings
312
+ remain an OpenAI Platform call and consume the associated developer-account
313
+ quota. Batch transcription uses the authenticated ChatGPT backend and does not
314
+ require a developer API key.
314
315
 
315
316
  ### `POST /v1/embeddings`
316
317
 
@@ -328,13 +329,20 @@ but they work with the same Codex OAuth access token stored in
328
329
 
329
330
  The response matches the official embeddings shape:
330
331
  `{ "object": "list", "data": [{ "object": "embedding", ... }], "usage": ... }`.
332
+ The request is accounted against the OpenAI Platform organization returned by
333
+ the API; ChatGPT OAuth authenticates it but does not include it in a ChatGPT
334
+ subscription.
331
335
 
332
- ### `POST /v1/audio/transcriptions`
336
+ ### `POST /backend-api/transcribe`
333
337
 
334
338
  **SDK method**: `client.audio.transcriptions.create(...)`
335
339
 
336
- **Status**: Supported for non-streaming calls. Verified with multipart upload
337
- using `gpt-4o-mini-transcribe`.
340
+ **Status**: Supported for non-streaming ChatGPT batch transcription. The SDK
341
+ uploads multipart audio with the OAuth bearer and `ChatGPT-Account-ID`, and
342
+ supports the `model`, `language`, `prompt`, `temperature`, and `json`/`text`
343
+ response options used by Codex Agent. Streaming, timestamps, speaker references,
344
+ chunking, SRT, and VTT are rejected locally rather than falling back to a
345
+ billable Platform endpoint.
338
346
 
339
347
  ### `POST /v1/audio/speech`
340
348
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-backend-sdk"
7
- version = "0.3.7"
7
+ version = "0.3.8"
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,7 @@
1
+ import pytest
2
+
1
3
  from codex_backend_sdk import CreateEmbeddingResponse, OpenAI, Transcription
4
+ from codex_backend_sdk._utils import CodexBackendUnsupportedParameterError
2
5
  from codex_backend_sdk.storage import TokenStore
3
6
 
4
7
 
@@ -20,7 +23,7 @@ class FakeOpenAIClient(OpenAI):
20
23
  def __init__(self):
21
24
  super().__init__(model="gpt-test")
22
25
  self.openai_posts = []
23
- self.openai_raw_posts = []
26
+ self.chatgpt_raw_posts = []
24
27
  self._set_store(TokenStore(
25
28
  access_token="chatgpt-token",
26
29
  refresh_token="refresh-token",
@@ -37,8 +40,8 @@ class FakeOpenAIClient(OpenAI):
37
40
  "usage": {"prompt_tokens": 1, "total_tokens": 1},
38
41
  }
39
42
 
40
- def _post_openai_raw(self, path, **kwargs):
41
- self.openai_raw_posts.append((path, kwargs, self._openai_headers()))
43
+ def _post_chatgpt_raw(self, path, **kwargs):
44
+ self.chatgpt_raw_posts.append((path, kwargs, self._auth_headers()))
42
45
  return FakeJSONResponse()
43
46
 
44
47
 
@@ -65,7 +68,7 @@ def test_embeddings_create_posts_to_openai_with_codex_oauth_token():
65
68
  assert headers["Authorization"] == "Bearer chatgpt-token"
66
69
 
67
70
 
68
- def test_audio_transcriptions_create_posts_multipart_like_official_sdk():
71
+ def test_audio_transcriptions_create_posts_to_chatgpt_backend():
69
72
  client = FakeOpenAIClient()
70
73
 
71
74
  response = client.audio.transcriptions.create(
@@ -77,8 +80,8 @@ def test_audio_transcriptions_create_posts_multipart_like_official_sdk():
77
80
 
78
81
  assert isinstance(response, Transcription)
79
82
  assert response.text == "hello"
80
- path, kwargs, headers = client.openai_raw_posts[0]
81
- assert path == "/audio/transcriptions"
83
+ path, kwargs, headers = client.chatgpt_raw_posts[0]
84
+ assert path == "/transcribe"
82
85
  assert kwargs["files"]["file"] == ("clip.wav", b"wav", "audio/wav")
83
86
  assert kwargs["data"] == {
84
87
  "model": "gpt-4o-mini-transcribe",
@@ -86,3 +89,34 @@ def test_audio_transcriptions_create_posts_multipart_like_official_sdk():
86
89
  "response_format": "json",
87
90
  }
88
91
  assert headers["Authorization"] == "Bearer chatgpt-token"
92
+ assert headers["ChatGPT-Account-ID"] == "account-id"
93
+
94
+
95
+ def test_audio_transcriptions_text_format_returns_string():
96
+ client = FakeOpenAIClient()
97
+
98
+ response = client.audio.transcriptions.create(
99
+ model="gpt-4o-mini-transcribe",
100
+ file=("clip.wav", b"wav", "audio/wav"),
101
+ response_format="text",
102
+ )
103
+
104
+ assert response == "hello"
105
+
106
+
107
+ def test_audio_transcriptions_rejects_unsupported_options():
108
+ client = FakeOpenAIClient()
109
+
110
+ with pytest.raises(CodexBackendUnsupportedParameterError, match="stream"):
111
+ client.audio.transcriptions.create(
112
+ model="gpt-4o-mini-transcribe",
113
+ file=("clip.wav", b"wav", "audio/wav"),
114
+ stream=True,
115
+ )
116
+
117
+ with pytest.raises(CodexBackendUnsupportedParameterError, match="json.*text"):
118
+ client.audio.transcriptions.create(
119
+ model="gpt-4o-mini-transcribe",
120
+ file=("clip.wav", b"wav", "audio/wav"),
121
+ response_format="srt",
122
+ )
@@ -160,7 +160,7 @@ wheels = [
160
160
 
161
161
  [[package]]
162
162
  name = "codex-backend-sdk"
163
- version = "0.3.7"
163
+ version = "0.3.8"
164
164
  source = { editable = "." }
165
165
  dependencies = [
166
166
  { name = "pydantic" },