codex-backend-sdk 0.3.6__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.6 → codex_backend_sdk-0.3.8}/CHANGELOG.md +25 -0
  2. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/PKG-INFO +4 -4
  3. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/README.md +3 -3
  4. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/__init__.py +1 -1
  5. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_client.py +21 -2
  6. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_models.py +27 -1
  7. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/openai_oauth.py +41 -29
  8. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/realtime.py +20 -6
  9. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/docs/backend-api.md +26 -11
  10. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/pyproject.toml +1 -1
  11. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_openai_oauth_resources.py +40 -6
  12. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_realtime_resource.py +28 -1
  13. codex_backend_sdk-0.3.8/uv.lock +792 -0
  14. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/.gitignore +0 -0
  15. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/LICENSE +0 -0
  16. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_streaming.py +0 -0
  17. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_transport.py +0 -0
  18. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_utils.py +0 -0
  19. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/codex_client.py +0 -0
  20. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/oauth.py +0 -0
  21. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/pkce.py +0 -0
  22. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/__init__.py +0 -0
  23. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
  24. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/codex.py +0 -0
  25. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/files.py +0 -0
  26. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/models.py +0 -0
  27. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/responses.py +0 -0
  28. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/storage.py +0 -0
  29. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/examples/agent.py +0 -0
  30. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/conftest.py +0 -0
  31. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_account_info.py +0 -0
  32. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_basic.py +0 -0
  33. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_client_retry.py +0 -0
  34. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_codex_resources.py +0 -0
  35. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_conversation.py +0 -0
  36. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_files_resource.py +0 -0
  37. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_reasoning.py +0 -0
  38. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_responses_resource.py +0 -0
  39. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_structured_output.py +0 -0
  40. {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_tools.py +0 -0
@@ -2,6 +2,31 @@
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
+
18
+ ## [0.3.7] - 2026-07-17
19
+
20
+ ### Added
21
+ - Added typed Realtime call results through `RealtimeCallResponse.answer_sdp` and `RealtimeCallResponse.call_id`.
22
+ - Added Codex AVAS session payload support, including automatic removal of the server-generated session `id` and the required `quicksilver` query parameters.
23
+
24
+ ### Documentation
25
+ - Documented that the ChatGPT-authenticated Codex WebRTC route is experimental and rollout-dependent, while the public Realtime WebSocket route still requires a developer API key.
26
+
27
+ ### Tests
28
+ - Added coverage for SDP response parsing, call ID validation, AVAS payload construction, and invalid session payloads.
29
+
5
30
  ## [0.3.6] - 2026-07-11
6
31
 
7
32
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-backend-sdk
3
- Version: 0.3.6
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
@@ -186,10 +186,10 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
186
186
  | `POST /backend-api/codex/responses/compact` | `client.responses.compact(...)` | Codex-specific helper for encrypted context compaction. |
187
187
  | `POST /backend-api/codex/memories/trace_summarize` | `client.codex.memories.trace_summarize(...)` | Raw Codex memory trace summarization helper. |
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
- | `POST /backend-api/codex/realtime/calls` | `client.realtime.calls.create(...)` | OpenAI-shaped SDP call creation for realtime sessions. |
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. |
@@ -160,10 +160,10 @@ resources (`responses`, `models`, `realtime`) or Codex-only resources (`codex`).
160
160
  | `POST /backend-api/codex/responses/compact` | `client.responses.compact(...)` | Codex-specific helper for encrypted context compaction. |
161
161
  | `POST /backend-api/codex/memories/trace_summarize` | `client.codex.memories.trace_summarize(...)` | Raw Codex memory trace summarization helper. |
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
- | `POST /backend-api/codex/realtime/calls` | `client.realtime.calls.create(...)` | OpenAI-shaped SDP call creation for realtime sessions. |
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.6"
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)
@@ -304,4 +304,30 @@ class BinaryResponseContent:
304
304
 
305
305
 
306
306
  class RealtimeCallResponse(BinaryResponseContent):
307
- """Binary SDP response returned by ``client.realtime.calls.create``."""
307
+ """SDP answer and call identifier returned by Realtime call creation."""
308
+
309
+ @property
310
+ def answer_sdp(self) -> str:
311
+ return self.text
312
+
313
+ @property
314
+ def call_id(self) -> str:
315
+ location = self.response.headers.get("Location")
316
+ if not location:
317
+ raise RuntimeError("Realtime call response is missing the Location header.")
318
+ path = location.split("?", 1)[0].rstrip("/")
319
+ call_id = path.rsplit("/", 1)[-1]
320
+ if call_id.startswith("rtc_") or _is_uuid(call_id):
321
+ return call_id
322
+ raise RuntimeError(
323
+ f"Realtime call Location does not contain a valid call id: {location}"
324
+ )
325
+
326
+
327
+ def _is_uuid(value: str) -> bool:
328
+ if len(value) != 36:
329
+ return False
330
+ return all(
331
+ char == "-" if index in {8, 13, 18, 23} else char in "0123456789abcdefABCDEF"
332
+ for index, char in enumerate(value)
333
+ )
@@ -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
@@ -35,6 +35,12 @@ class Realtime:
35
35
 
36
36
 
37
37
  class RealtimeCalls:
38
+ """Experimental Codex WebRTC call creation.
39
+
40
+ Availability depends on the Realtime call route enabled for the authenticated
41
+ ChatGPT account. A correctly authenticated request may still return 404 while
42
+ the backend is not rolled out.
43
+ """
38
44
  def __init__(self, client: CodexClient) -> None:
39
45
  self._client = client
40
46
 
@@ -65,15 +71,23 @@ class RealtimeCalls:
65
71
  )
66
72
  return RealtimeCallResponse(response)
67
73
 
74
+ session_payload = _jsonable(session)
75
+ if not isinstance(session_payload, dict):
76
+ raise TypeError("Expected `session` to serialize to a JSON object.")
77
+ session_payload.pop("id", None)
78
+ body = {
79
+ "sdp": sdp,
80
+ "session": session_payload,
81
+ **(_jsonable(extra_body) if extra_body else {}),
82
+ }
83
+ query = {"intent": "quicksilver", "architecture": "avas"}
84
+ if extra_query:
85
+ query.update(extra_query)
68
86
  response = self._client._post_raw(
69
87
  "/realtime/calls",
70
- body={
71
- "sdp": sdp,
72
- "session": _jsonable(session),
73
- **(_jsonable(extra_body) if extra_body else {}),
74
- },
88
+ body=body,
75
89
  headers={"Accept": "application/sdp", **(extra_headers or {})},
76
- params=extra_query,
90
+ params=query,
77
91
  timeout=timeout,
78
92
  )
79
93
  return RealtimeCallResponse(response)
@@ -264,14 +264,21 @@ Realtime audio/video call initiation.
264
264
 
265
265
  **SDK method**: `client.realtime.calls.create(...)`
266
266
 
267
- **Status**: Supported. The SDK follows the official OpenAI SDK shape:
267
+ **Status**: Experimental and rollout-dependent. The SDK follows the Codex
268
+ client protocol:
268
269
 
269
270
  - plain SDP offer: `client.realtime.calls.create(sdp=offer_sdp)`
270
- - SDP offer plus session payload:
271
+ - AVAS SDP offer plus session payload:
271
272
  `client.realtime.calls.create(sdp=offer_sdp, session={...})`
272
273
 
273
- The response is returned as binary SDP content with `.content`, `.text`,
274
- `.read()`, `.iter_bytes()`, and `.write_to_file(...)` helpers.
274
+ The OAuth-authenticated ChatGPT route is not enabled for every account and may
275
+ return `404 Not Found` until Codex supplies an experimental WebRTC call base URL.
276
+ This is distinct from the public Realtime WebSocket route, which currently
277
+ requires a developer API key.
278
+
279
+ The response exposes `.answer_sdp` and `.call_id`, while preserving the binary
280
+ helpers `.content`, `.text`, `.read()`, `.iter_bytes()`, and
281
+ `.write_to_file(...)`.
275
282
 
276
283
  ---
277
284
 
@@ -299,11 +306,12 @@ continues to work but Voice v2 requires a separately provisioned API key.
299
306
 
300
307
  ---
301
308
 
302
- ## OpenAI API Endpoints With Codex OAuth
309
+ ## Embeddings and Transcription
303
310
 
304
- These endpoints live under `https://api.openai.com/v1`, not the ChatGPT backend,
305
- but they work with the same Codex OAuth access token stored in
306
- `~/.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.
307
315
 
308
316
  ### `POST /v1/embeddings`
309
317
 
@@ -321,13 +329,20 @@ but they work with the same Codex OAuth access token stored in
321
329
 
322
330
  The response matches the official embeddings shape:
323
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.
324
335
 
325
- ### `POST /v1/audio/transcriptions`
336
+ ### `POST /backend-api/transcribe`
326
337
 
327
338
  **SDK method**: `client.audio.transcriptions.create(...)`
328
339
 
329
- **Status**: Supported for non-streaming calls. Verified with multipart upload
330
- 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.
331
346
 
332
347
  ### `POST /v1/audio/speech`
333
348
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-backend-sdk"
7
- version = "0.3.6"
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
+ )
@@ -6,6 +6,7 @@ class FakeResponse:
6
6
  content = b"answer-sdp"
7
7
  text = "answer-sdp"
8
8
  encoding = "utf-8"
9
+ headers = {"Location": "/v1/realtime/calls/calls/rtc_test"}
9
10
 
10
11
  def json(self, **kwargs):
11
12
  return {"ok": True}
@@ -43,6 +44,8 @@ def test_realtime_calls_create_posts_plain_sdp_like_official_sdk():
43
44
 
44
45
  assert isinstance(response, RealtimeCallResponse)
45
46
  assert response.read() == b"answer-sdp"
47
+ assert response.answer_sdp == "answer-sdp"
48
+ assert response.call_id == "rtc_test"
46
49
  path, kwargs = client.raw_posts[0]
47
50
  assert path == "/realtime/calls"
48
51
  assert kwargs["content"] == b"offer-sdp"
@@ -55,7 +58,7 @@ def test_realtime_calls_create_posts_session_as_backend_json():
55
58
 
56
59
  client.realtime.calls.create(
57
60
  sdp="offer-sdp",
58
- session={"type": "realtime", "model": "gpt-realtime-1.5"},
61
+ session={"id": "session-id", "type": "realtime", "model": "gpt-realtime-1.5"},
59
62
  )
60
63
 
61
64
  path, kwargs = client.raw_posts[0]
@@ -64,9 +67,33 @@ def test_realtime_calls_create_posts_session_as_backend_json():
64
67
  "sdp": "offer-sdp",
65
68
  "session": {"type": "realtime", "model": "gpt-realtime-1.5"},
66
69
  }
70
+ assert kwargs["params"] == {"intent": "quicksilver", "architecture": "avas"}
67
71
  assert kwargs["headers"]["Accept"] == "application/sdp"
68
72
 
69
73
 
74
+ def test_realtime_calls_create_rejects_non_object_session():
75
+ client = FakeRealtimeClient()
76
+
77
+ try:
78
+ client.realtime.calls.create(sdp="offer-sdp", session=["invalid"])
79
+ except TypeError as exc:
80
+ assert str(exc) == "Expected `session` to serialize to a JSON object."
81
+ else:
82
+ raise AssertionError("Expected non-object session to fail")
83
+
84
+
85
+ def test_realtime_call_response_requires_valid_location():
86
+ response = FakeResponse()
87
+ response.headers = {"Location": "/v1/realtime/calls/not-a-call"}
88
+
89
+ try:
90
+ RealtimeCallResponse(response).call_id
91
+ except RuntimeError as exc:
92
+ assert "does not contain a valid call id" in str(exc)
93
+ else:
94
+ raise AssertionError("Expected invalid Location to fail")
95
+
96
+
70
97
  def test_realtime_websocket_uses_api_key_exchanged_during_oauth():
71
98
  client = FakeRealtimeClient()
72
99