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.
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/CHANGELOG.md +25 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/PKG-INFO +4 -4
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/README.md +3 -3
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/__init__.py +1 -1
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_client.py +21 -2
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_models.py +27 -1
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/openai_oauth.py +41 -29
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/realtime.py +20 -6
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/docs/backend-api.md +26 -11
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/pyproject.toml +1 -1
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_openai_oauth_resources.py +40 -6
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_realtime_resource.py +28 -1
- codex_backend_sdk-0.3.8/uv.lock +792 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/.gitignore +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/LICENSE +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_transport.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/_utils.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/codex_client.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/codex.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/models.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/responses.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/examples/agent.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_account_info.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_client_retry.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_codex_resources.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_responses_resource.py +0 -0
- {codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/tests/test_structured_output.py +0 -0
- {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.
|
|
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(...)` |
|
|
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`;
|
|
192
|
-
| `POST /
|
|
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(...)` |
|
|
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`;
|
|
166
|
-
| `POST /
|
|
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. |
|
|
@@ -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
|
-
|
|
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
|
-
"""
|
|
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
|
+
)
|
{codex_backend_sdk-0.3.6 → codex_backend_sdk-0.3.8}/codex_backend_sdk/resources/openai_oauth.py
RENAMED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
"""OpenAI
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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=
|
|
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**:
|
|
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
|
|
274
|
-
|
|
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
|
-
##
|
|
309
|
+
## Embeddings and Transcription
|
|
303
310
|
|
|
304
|
-
These
|
|
305
|
-
|
|
306
|
-
|
|
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 /
|
|
336
|
+
### `POST /backend-api/transcribe`
|
|
326
337
|
|
|
327
338
|
**SDK method**: `client.audio.transcriptions.create(...)`
|
|
328
339
|
|
|
329
|
-
**Status**: Supported for non-streaming
|
|
330
|
-
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
41
|
-
self.
|
|
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
|
|
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.
|
|
81
|
-
assert path == "/
|
|
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
|
|