codex-backend-sdk 0.5.4__tar.gz → 0.5.6__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.5.4 → codex_backend_sdk-0.5.6}/CHANGELOG.md +28 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/PKG-INFO +45 -4
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/README.md +44 -3
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/__init__.py +1 -1
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/_client.py +3 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/_models.py +1 -1
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/_streaming.py +15 -5
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/_responses_payloads.py +2 -2
- codex_backend_sdk-0.5.6/codex_backend_sdk/resources/_responses_routing.py +45 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/codex.py +4 -5
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/realtime.py +3 -17
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/responses.py +88 -19
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/docs/api-reference.md +6 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/docs/backend-api.md +11 -5
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/pyproject.toml +1 -1
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_codex_resources.py +20 -23
- codex_backend_sdk-0.5.6/tests/test_prompt_cache_usage.py +38 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_realtime_resource.py +22 -20
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_responses_resource.py +53 -0
- codex_backend_sdk-0.5.6/tests/test_responses_routing.py +185 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/uv.lock +1 -1
- codex_backend_sdk-0.5.4/tests/test_prompt_cache_usage.py +0 -16
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/.gitignore +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/LICENSE +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/_transport.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/_utils.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/codex_client.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/chatgpt.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/chatgpt_apps.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/chatgpt_connectors.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/chatgpt_plugin_bundles.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/chatgpt_plugin_shares.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/chatgpt_plugins.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/chatgpt_writing_blocks.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/codex_web_search.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/images.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/models.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/openai_oauth.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/remote_control.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/responses_websocket.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/worktree_snapshots.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/docs/desktop-endpoint-inventory.md +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/docs/endpoint-coverage.md +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/docs/openai-compatibility.md +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/examples/agent.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_account_info.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_apps.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_connectors.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_plugin_bundles.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_plugin_shares.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_plugins.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_projects_gizmos.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_resources.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_search_files.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_chatgpt_writing_blocks.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_client_retry.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_codex_web_search.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_hosted_mcp.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_images.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_oauth.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_openai_compatibility.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_openai_oauth_resources.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_remote_control_resource.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_responses_websocket.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_structured_output.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_tools.py +0 -0
- {codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/tests/test_worktree_snapshots.py +0 -0
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.6] - 2026-10-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Preserve an absent or null `input_tokens_details.cached_tokens` as `None`
|
|
9
|
+
instead of inventing a zero. Explicit API-reported zero remains zero, so
|
|
10
|
+
consumers can exclude unmeasurable calls from cache statistics without
|
|
11
|
+
confusing them with measured cache misses. The SDK computes no cache rate
|
|
12
|
+
or moving average.
|
|
13
|
+
|
|
14
|
+
## [0.5.5] - 2026-10-22
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Added `client.responses.turn()` for ephemeral sticky routing within one logical
|
|
18
|
+
HTTP Responses turn, isolated across concurrent turns and invalidated when
|
|
19
|
+
authentication or cache affinity changes.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Forward Realtime model identifiers unchanged instead of maintaining a local
|
|
23
|
+
allowlist. Newly available Codex models, including `gpt-live-1.1`, no longer
|
|
24
|
+
require an SDK update; the backend determines availability and authorization.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Supply the HTTP `session-id` cache-affinity header from `prompt_cache_key`
|
|
28
|
+
unless an explicit request or client header already sets it.
|
|
29
|
+
- Route reset-credit requests through the authenticated WHAM API.
|
|
30
|
+
- Preserve backend remote-compaction errors and close the response stream.
|
|
31
|
+
- Align the exported package version with the distribution metadata.
|
|
32
|
+
|
|
5
33
|
## [0.5.4] - 2026-09-24
|
|
6
34
|
|
|
7
35
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codex-backend-sdk
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Unofficial Python SDK for the ChatGPT Codex backend API
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -51,6 +51,12 @@ The package intentionally separates three kinds of API:
|
|
|
51
51
|
|
|
52
52
|
## Install
|
|
53
53
|
|
|
54
|
+
```bash
|
|
55
|
+
pip install --upgrade codex-backend-sdk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For an editable checkout:
|
|
59
|
+
|
|
54
60
|
```bash
|
|
55
61
|
git clone https://github.com/B4PT0R/codex-backend-sdk.git
|
|
56
62
|
cd codex-backend-sdk
|
|
@@ -492,6 +498,36 @@ The ChatGPT read-aloud service is smaller than the public speech API: it exposes
|
|
|
492
498
|
language and playback speed, not arbitrary model and voice selection. It can
|
|
493
499
|
return a typed in-memory object, bytes, `BytesIO`, a data URI, or a file path.
|
|
494
500
|
|
|
501
|
+
### Turn-scoped HTTP routing
|
|
502
|
+
|
|
503
|
+
For an agent turn with several generations (tools or retries), use an isolated
|
|
504
|
+
Responses resource. Continue sending the complete input as usual:
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
with client.responses.turn() as turn:
|
|
508
|
+
first = turn.create(input=items, prompt_cache_key="conversation-id")
|
|
509
|
+
# Append model/tool results to items, then generate again in the same turn.
|
|
510
|
+
following = turn.create(input=items, prompt_cache_key="conversation-id")
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The resource captures the first `x-codex-turn-state` from HTTP response headers
|
|
514
|
+
or `response.metadata` events and replays it unchanged within this scope. The
|
|
515
|
+
opaque routing token is never added to model input or persisted. Leaving the
|
|
516
|
+
scope closes it; a new turn starts without the previous token. Authentication
|
|
517
|
+
or cache-affinity changes invalidate it. Separate scopes do not share tokens,
|
|
518
|
+
even on the same client. Streaming callers must consume the metadata event for
|
|
519
|
+
an SSE-only token to be captured. Compaction remains a separate backend request
|
|
520
|
+
and does not inherit the turn token; this scope does not manage WebSockets.
|
|
521
|
+
|
|
522
|
+
`prompt_cache_key` also supplies the HTTP `session-id` affinity header unless
|
|
523
|
+
an explicit request/client header overrides it (case-insensitively). These are
|
|
524
|
+
routing mechanisms, not a guarantee of a cache hit or reduced quota usage.
|
|
525
|
+
|
|
526
|
+
`response.usage.input_tokens_details.cached_tokens` preserves the API's cache
|
|
527
|
+
read measurement: an absent or null value is `None`, while an explicitly reported
|
|
528
|
+
zero remains `0`. The SDK does not calculate cache percentages or moving averages;
|
|
529
|
+
applications should exclude missing measurements from those calculations.
|
|
530
|
+
|
|
495
531
|
## Advanced transports
|
|
496
532
|
|
|
497
533
|
### Realtime voice
|
|
@@ -512,9 +548,14 @@ with client.live.sideband.connect(session_id=live.session.id) as sideband:
|
|
|
512
548
|
print(event.type)
|
|
513
549
|
```
|
|
514
550
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
551
|
+
Model identifiers are forwarded unchanged: the backend decides which models
|
|
552
|
+
are available to your account, so new Codex models do not require an SDK update.
|
|
553
|
+
Earlier call-creation probes accepted `gpt-live-1-codex` and
|
|
554
|
+
`gpt-live-1-boulder-alpha`; these examples are not an exhaustive catalog.
|
|
555
|
+
A `gpt-live-1.1` probe on October 22, 2026 also returned an SDP answer and
|
|
556
|
+
connected WebRTC, but the server then rejected the voice session with
|
|
557
|
+
`forbidden: Voice session access denied.` Call creation alone does not prove
|
|
558
|
+
that an account can receive model responses. `client.live` follows the current
|
|
518
559
|
`openai-python` creation, response, and sideband idioms while adapting them to
|
|
519
560
|
the ChatGPT OAuth call transport.
|
|
520
561
|
|
|
@@ -22,6 +22,12 @@ The package intentionally separates three kinds of API:
|
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
|
+
```bash
|
|
26
|
+
pip install --upgrade codex-backend-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For an editable checkout:
|
|
30
|
+
|
|
25
31
|
```bash
|
|
26
32
|
git clone https://github.com/B4PT0R/codex-backend-sdk.git
|
|
27
33
|
cd codex-backend-sdk
|
|
@@ -463,6 +469,36 @@ The ChatGPT read-aloud service is smaller than the public speech API: it exposes
|
|
|
463
469
|
language and playback speed, not arbitrary model and voice selection. It can
|
|
464
470
|
return a typed in-memory object, bytes, `BytesIO`, a data URI, or a file path.
|
|
465
471
|
|
|
472
|
+
### Turn-scoped HTTP routing
|
|
473
|
+
|
|
474
|
+
For an agent turn with several generations (tools or retries), use an isolated
|
|
475
|
+
Responses resource. Continue sending the complete input as usual:
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
with client.responses.turn() as turn:
|
|
479
|
+
first = turn.create(input=items, prompt_cache_key="conversation-id")
|
|
480
|
+
# Append model/tool results to items, then generate again in the same turn.
|
|
481
|
+
following = turn.create(input=items, prompt_cache_key="conversation-id")
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The resource captures the first `x-codex-turn-state` from HTTP response headers
|
|
485
|
+
or `response.metadata` events and replays it unchanged within this scope. The
|
|
486
|
+
opaque routing token is never added to model input or persisted. Leaving the
|
|
487
|
+
scope closes it; a new turn starts without the previous token. Authentication
|
|
488
|
+
or cache-affinity changes invalidate it. Separate scopes do not share tokens,
|
|
489
|
+
even on the same client. Streaming callers must consume the metadata event for
|
|
490
|
+
an SSE-only token to be captured. Compaction remains a separate backend request
|
|
491
|
+
and does not inherit the turn token; this scope does not manage WebSockets.
|
|
492
|
+
|
|
493
|
+
`prompt_cache_key` also supplies the HTTP `session-id` affinity header unless
|
|
494
|
+
an explicit request/client header overrides it (case-insensitively). These are
|
|
495
|
+
routing mechanisms, not a guarantee of a cache hit or reduced quota usage.
|
|
496
|
+
|
|
497
|
+
`response.usage.input_tokens_details.cached_tokens` preserves the API's cache
|
|
498
|
+
read measurement: an absent or null value is `None`, while an explicitly reported
|
|
499
|
+
zero remains `0`. The SDK does not calculate cache percentages or moving averages;
|
|
500
|
+
applications should exclude missing measurements from those calculations.
|
|
501
|
+
|
|
466
502
|
## Advanced transports
|
|
467
503
|
|
|
468
504
|
### Realtime voice
|
|
@@ -483,9 +519,14 @@ with client.live.sideband.connect(session_id=live.session.id) as sideband:
|
|
|
483
519
|
print(event.type)
|
|
484
520
|
```
|
|
485
521
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
522
|
+
Model identifiers are forwarded unchanged: the backend decides which models
|
|
523
|
+
are available to your account, so new Codex models do not require an SDK update.
|
|
524
|
+
Earlier call-creation probes accepted `gpt-live-1-codex` and
|
|
525
|
+
`gpt-live-1-boulder-alpha`; these examples are not an exhaustive catalog.
|
|
526
|
+
A `gpt-live-1.1` probe on October 22, 2026 also returned an SDP answer and
|
|
527
|
+
connected WebRTC, but the server then rejected the voice session with
|
|
528
|
+
`forbidden: Voice session access denied.` Call creation alone does not prove
|
|
529
|
+
that an account can receive model responses. `client.live` follows the current
|
|
489
530
|
`openai-python` creation, response, and sideband idioms while adapting them to
|
|
490
531
|
the ChatGPT OAuth call transport.
|
|
491
532
|
|
|
@@ -48,6 +48,7 @@ class CodexClient:
|
|
|
48
48
|
from .resources.responses import Responses
|
|
49
49
|
|
|
50
50
|
self._store = store
|
|
51
|
+
self._auth_revision = 0
|
|
51
52
|
self._timeout = timeout
|
|
52
53
|
self._max_retries = max_retries
|
|
53
54
|
self._retry_base_delay = retry_base_delay
|
|
@@ -226,6 +227,7 @@ class CodexClient:
|
|
|
226
227
|
|
|
227
228
|
cleared = clear_tokens()
|
|
228
229
|
self._store = None
|
|
230
|
+
self._auth_revision += 1
|
|
229
231
|
self._session.headers.pop("Authorization", None)
|
|
230
232
|
self._session.headers.pop("ChatGPT-Account-ID", None)
|
|
231
233
|
return cleared
|
|
@@ -245,6 +247,7 @@ class CodexClient:
|
|
|
245
247
|
return self.logout()
|
|
246
248
|
|
|
247
249
|
def _set_store(self, store: TokenStore) -> None:
|
|
250
|
+
self._auth_revision += 1
|
|
248
251
|
self._store = store
|
|
249
252
|
self._session.headers.update(self._auth_headers())
|
|
250
253
|
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from collections.abc import Iterator
|
|
7
|
-
from typing import Any, Optional
|
|
7
|
+
from typing import Any, Callable, Optional
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
10
|
|
|
@@ -14,8 +14,12 @@ from ._models import ResponseStreamEvent
|
|
|
14
14
|
class ResponseEventStream(Iterator[ResponseStreamEvent]):
|
|
15
15
|
"""Closeable/context-managed event stream compatible with OpenAI streams."""
|
|
16
16
|
|
|
17
|
-
def __init__(
|
|
17
|
+
def __init__(
|
|
18
|
+
self, response: requests.Response, *,
|
|
19
|
+
on_event: Optional[Callable[[ResponseStreamEvent], None]] = None,
|
|
20
|
+
) -> None:
|
|
18
21
|
self.response = response
|
|
22
|
+
self._on_event = on_event
|
|
19
23
|
self._events = (
|
|
20
24
|
ResponseStreamEvent.model_validate(payload)
|
|
21
25
|
for payload in iter_sse_payloads(response)
|
|
@@ -25,7 +29,10 @@ class ResponseEventStream(Iterator[ResponseStreamEvent]):
|
|
|
25
29
|
return self
|
|
26
30
|
|
|
27
31
|
def __next__(self) -> ResponseStreamEvent:
|
|
28
|
-
|
|
32
|
+
event = next(self._events)
|
|
33
|
+
if self._on_event is not None:
|
|
34
|
+
self._on_event(event)
|
|
35
|
+
return event
|
|
29
36
|
|
|
30
37
|
def close(self) -> None:
|
|
31
38
|
close = getattr(self.response, "close", None)
|
|
@@ -39,8 +46,11 @@ class ResponseEventStream(Iterator[ResponseStreamEvent]):
|
|
|
39
46
|
self.close()
|
|
40
47
|
|
|
41
48
|
|
|
42
|
-
def stream_response_events(
|
|
43
|
-
|
|
49
|
+
def stream_response_events(
|
|
50
|
+
response: requests.Response, *,
|
|
51
|
+
on_event: Optional[Callable[[ResponseStreamEvent], None]] = None,
|
|
52
|
+
) -> ResponseEventStream:
|
|
53
|
+
return ResponseEventStream(response, on_event=on_event)
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
def iter_sse_payloads(response: requests.Response) -> Iterator[dict[str, Any]]:
|
|
@@ -251,7 +251,7 @@ def _event_error_message(event: ResponseStreamEvent) -> str:
|
|
|
251
251
|
response = _event_response_dict(event)
|
|
252
252
|
error = response.get("error") or getattr(event, "error", None) or {}
|
|
253
253
|
if isinstance(error, dict):
|
|
254
|
-
return error.get("message", "Response failed"
|
|
254
|
+
return error.get("message") or getattr(event, "message", None) or "Response failed"
|
|
255
255
|
return str(error)
|
|
256
256
|
|
|
257
257
|
|
|
@@ -302,7 +302,7 @@ def _usage_from_backend(raw: Any) -> ResponseUsage:
|
|
|
302
302
|
output_tokens=raw.get("output_tokens", 0),
|
|
303
303
|
total_tokens=raw.get("total_tokens", 0),
|
|
304
304
|
input_tokens_details=TokenDetails(
|
|
305
|
-
cached_tokens=(raw.get("input_tokens_details") or {}).get("cached_tokens"
|
|
305
|
+
cached_tokens=(raw.get("input_tokens_details") or {}).get("cached_tokens"),
|
|
306
306
|
cache_write_tokens=(raw.get("input_tokens_details") or {}).get(
|
|
307
307
|
"cache_write_tokens",
|
|
308
308
|
0,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Ephemeral sticky routing for one Responses turn, never shared across turns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
TURN_STATE_HEADER = "x-codex-turn-state"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def header_value(headers: Any, name: str, default=None) -> Any:
|
|
13
|
+
if not isinstance(headers, Mapping):
|
|
14
|
+
return default
|
|
15
|
+
return next((value for key, value in headers.items() if key.lower() == name), default)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ResponsesTurnState:
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._lock = Lock()
|
|
21
|
+
self._identity = None
|
|
22
|
+
self._value = None
|
|
23
|
+
self._epoch = 0
|
|
24
|
+
self._closed = False
|
|
25
|
+
|
|
26
|
+
def prepare(self, identity: tuple) -> tuple[int, Optional[str]]:
|
|
27
|
+
with self._lock:
|
|
28
|
+
if self._closed:
|
|
29
|
+
raise RuntimeError("Responses turn is closed.")
|
|
30
|
+
if self._identity != identity:
|
|
31
|
+
self._identity, self._value = identity, None
|
|
32
|
+
self._epoch += 1
|
|
33
|
+
return self._epoch, self._value
|
|
34
|
+
|
|
35
|
+
def capture(self, epoch: int, value: Any) -> None:
|
|
36
|
+
if not isinstance(value, str) or not value or "\r" in value or "\n" in value:
|
|
37
|
+
return
|
|
38
|
+
with self._lock:
|
|
39
|
+
if not self._closed and self._epoch == epoch and self._value is None:
|
|
40
|
+
self._value = value
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
with self._lock:
|
|
44
|
+
self._closed = True
|
|
45
|
+
self._identity = self._value = None
|
|
@@ -288,7 +288,7 @@ class CodexRateLimitResetCredits:
|
|
|
288
288
|
|
|
289
289
|
def list(self) -> RateLimitResetCredits:
|
|
290
290
|
return RateLimitResetCredits.model_validate(
|
|
291
|
-
self._client.
|
|
291
|
+
self._client._get_wham("/wham/rate-limit-reset-credits")
|
|
292
292
|
)
|
|
293
293
|
|
|
294
294
|
def consume(
|
|
@@ -308,10 +308,9 @@ class CodexRateLimitResetCredits:
|
|
|
308
308
|
if credit_id is not None:
|
|
309
309
|
payload["credit_id"] = credit_id
|
|
310
310
|
return ConsumeRateLimitResetCreditResponse.model_validate(
|
|
311
|
-
self._client.
|
|
312
|
-
"/rate-limit-reset-credits/consume",
|
|
313
|
-
|
|
314
|
-
).json()
|
|
311
|
+
self._client._post_wham(
|
|
312
|
+
"/wham/rate-limit-reset-credits/consume", body=payload
|
|
313
|
+
)
|
|
315
314
|
)
|
|
316
315
|
|
|
317
316
|
def history(self, *, cursor: str | None = None) -> dict[str, Any]:
|
|
@@ -24,12 +24,6 @@ if TYPE_CHECKING:
|
|
|
24
24
|
from .._client import CodexClient
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
CODEX_REALTIME_V3_MODELS = frozenset({
|
|
28
|
-
"gpt-live-1-codex",
|
|
29
|
-
"gpt-live-1-boulder-alpha",
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
|
|
33
27
|
class Realtime:
|
|
34
28
|
"""Realtime resources matching the official OpenAI SDK surface where present."""
|
|
35
29
|
|
|
@@ -362,9 +356,8 @@ class RealtimeSideband:
|
|
|
362
356
|
class RealtimeCalls:
|
|
363
357
|
"""Codex WebRTC call creation over ChatGPT OAuth.
|
|
364
358
|
|
|
365
|
-
Realtime v3 uses the JSON call shape
|
|
366
|
-
|
|
367
|
-
on this OAuth-authenticated route.
|
|
359
|
+
Realtime v3 uses the JSON call shape. Model identifiers are forwarded
|
|
360
|
+
unchanged; availability and validation belong to the backend.
|
|
368
361
|
"""
|
|
369
362
|
def __init__(self, client: CodexClient) -> None:
|
|
370
363
|
self._client = client
|
|
@@ -381,7 +374,7 @@ class RealtimeCalls:
|
|
|
381
374
|
extra_body: Any = None,
|
|
382
375
|
timeout: Any = _UNSET,
|
|
383
376
|
) -> RealtimeCallResponse:
|
|
384
|
-
"""Create a Codex Realtime v3 call
|
|
377
|
+
"""Create a Codex Realtime v3 call with a backend-validated model.
|
|
385
378
|
|
|
386
379
|
The ChatGPT backend requires both ``session-id`` and ``thread-id`` on
|
|
387
380
|
v3 call creation. Fresh UUIDs are generated when callers do not need
|
|
@@ -393,13 +386,6 @@ class RealtimeCalls:
|
|
|
393
386
|
if not isinstance(session_payload, dict):
|
|
394
387
|
raise TypeError("Expected `session` to serialize to a JSON object.")
|
|
395
388
|
session_payload.pop("id", None)
|
|
396
|
-
model = session_payload.get("model")
|
|
397
|
-
if model not in CODEX_REALTIME_V3_MODELS:
|
|
398
|
-
supported = ", ".join(sorted(CODEX_REALTIME_V3_MODELS))
|
|
399
|
-
raise ValueError(
|
|
400
|
-
"Codex Realtime v3 over ChatGPT OAuth requires "
|
|
401
|
-
f"one of these `session.model` values: {supported}."
|
|
402
|
-
)
|
|
403
389
|
headers = {
|
|
404
390
|
"session-id": session_id or str(uuid.uuid4()),
|
|
405
391
|
"thread-id": thread_id or str(uuid.uuid4()),
|
{codex_backend_sdk-0.5.4 → codex_backend_sdk-0.5.6}/codex_backend_sdk/resources/responses.py
RENAMED
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import uuid
|
|
7
7
|
from collections.abc import Iterator
|
|
8
|
-
from
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from hashlib import sha256
|
|
10
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
9
11
|
|
|
10
12
|
from .._models import CompactedResponse, ParsedResponse, Response, ResponseStreamEvent
|
|
11
13
|
from .._streaming import stream_response_events
|
|
@@ -13,6 +15,7 @@ from .._utils import _UNSET, _default, _is_given, _jsonable, _reject_backend_uns
|
|
|
13
15
|
from ._responses_payloads import (
|
|
14
16
|
ResponsesCreateRequest,
|
|
15
17
|
_usage_from_backend,
|
|
18
|
+
_event_error_message,
|
|
16
19
|
collect_response,
|
|
17
20
|
merge_text_format,
|
|
18
21
|
normalize_input_item,
|
|
@@ -22,17 +25,47 @@ from ._responses_payloads import (
|
|
|
22
25
|
pydantic_to_format,
|
|
23
26
|
)
|
|
24
27
|
|
|
28
|
+
from ._responses_routing import ResponsesTurnState, TURN_STATE_HEADER, header_value
|
|
29
|
+
|
|
25
30
|
if TYPE_CHECKING:
|
|
26
31
|
from .._client import CodexClient
|
|
27
32
|
|
|
28
33
|
|
|
34
|
+
def _cache_affinity_headers(payload, extra_headers, default_headers=None):
|
|
35
|
+
headers = dict(extra_headers or {})
|
|
36
|
+
# Codex routes ChatGPT prompt caches using this header, not only the body key.
|
|
37
|
+
key = payload.get("prompt_cache_key")
|
|
38
|
+
if (
|
|
39
|
+
key
|
|
40
|
+
and not any(name.lower() == "session-id" for name in headers)
|
|
41
|
+
and not any(name.lower() == "session-id" for name in (default_headers or {}))
|
|
42
|
+
):
|
|
43
|
+
headers["session-id"] = key
|
|
44
|
+
return headers or None
|
|
45
|
+
|
|
46
|
+
|
|
29
47
|
class Responses:
|
|
30
|
-
def __init__(self, client: CodexClient) -> None:
|
|
48
|
+
def __init__(self, client: CodexClient, *, _turn_state: Optional[ResponsesTurnState] = None) -> None:
|
|
31
49
|
from .responses_websocket import ResponsesWebSocket
|
|
32
50
|
|
|
33
51
|
self._client = client
|
|
52
|
+
self._turn_state = _turn_state
|
|
34
53
|
self.websocket = ResponsesWebSocket(client)
|
|
35
54
|
|
|
55
|
+
@contextmanager
|
|
56
|
+
def turn(self) -> Iterator[Responses]:
|
|
57
|
+
"""Yield isolated HTTP Responses routing for one logical agent turn.
|
|
58
|
+
|
|
59
|
+
Reuse this resource for generations/retries within that turn, not across
|
|
60
|
+
turns. No routing token is persisted or placed in the model's input.
|
|
61
|
+
Compaction is a separate backend turn and does not inherit this state.
|
|
62
|
+
"""
|
|
63
|
+
state = ResponsesTurnState()
|
|
64
|
+
try:
|
|
65
|
+
yield Responses(self._client, _turn_state=state)
|
|
66
|
+
finally:
|
|
67
|
+
state.close()
|
|
68
|
+
|
|
36
69
|
def create(
|
|
37
70
|
self,
|
|
38
71
|
*,
|
|
@@ -113,15 +146,50 @@ class Responses:
|
|
|
113
146
|
payload = dict(request.payload)
|
|
114
147
|
if extra_body:
|
|
115
148
|
payload.update(_jsonable(extra_body))
|
|
149
|
+
defaults = self._client._session.headers
|
|
150
|
+
headers = _cache_affinity_headers(payload, extra_headers, defaults) or {}
|
|
151
|
+
state = self._turn_state
|
|
152
|
+
if state is not None:
|
|
153
|
+
def effective_header(name):
|
|
154
|
+
return header_value(headers, name, header_value(defaults, name))
|
|
155
|
+
|
|
156
|
+
# Also invalidate for explicit per-request authentication overrides. Keep
|
|
157
|
+
# only a fingerprint, never credentials, in this ephemeral routing state.
|
|
158
|
+
identity = (
|
|
159
|
+
self._client._auth_revision,
|
|
160
|
+
sha256(repr((
|
|
161
|
+
effective_header("authorization"), effective_header("chatgpt-account-id"),
|
|
162
|
+
)).encode()).digest(),
|
|
163
|
+
effective_header("session-id"),
|
|
164
|
+
payload.get("prompt_cache_key"),
|
|
165
|
+
)
|
|
166
|
+
epoch, value = state.prepare(identity)
|
|
167
|
+
explicit = effective_header(TURN_STATE_HEADER)
|
|
168
|
+
if explicit is not None:
|
|
169
|
+
state.capture(epoch, explicit)
|
|
170
|
+
elif value:
|
|
171
|
+
headers[TURN_STATE_HEADER] = value
|
|
172
|
+
|
|
116
173
|
response = self._client._post(
|
|
117
174
|
"/responses",
|
|
118
175
|
body=payload,
|
|
119
176
|
stream=True,
|
|
120
|
-
headers=
|
|
177
|
+
headers=headers or None,
|
|
121
178
|
params=extra_query,
|
|
122
179
|
timeout=timeout,
|
|
123
180
|
)
|
|
124
|
-
|
|
181
|
+
|
|
182
|
+
def capture(headers):
|
|
183
|
+
if state is not None and self._client._auth_revision == identity[0]:
|
|
184
|
+
state.capture(epoch, header_value(headers, TURN_STATE_HEADER))
|
|
185
|
+
|
|
186
|
+
capture(getattr(response, "headers", {}))
|
|
187
|
+
|
|
188
|
+
def on_event(event):
|
|
189
|
+
if event.type == "response.metadata":
|
|
190
|
+
capture(getattr(event, "headers", None))
|
|
191
|
+
|
|
192
|
+
events = stream_response_events(response, on_event=on_event if state is not None else None)
|
|
125
193
|
stream_enabled = bool(stream) if _is_given(stream) else False
|
|
126
194
|
|
|
127
195
|
if stream_enabled:
|
|
@@ -315,7 +383,7 @@ class Responses:
|
|
|
315
383
|
if not payload["input"] or payload["input"][-1].get("type") != "compaction_trigger":
|
|
316
384
|
payload["input"].append({"type": "compaction_trigger"})
|
|
317
385
|
|
|
318
|
-
headers =
|
|
386
|
+
headers = _cache_affinity_headers(payload, extra_headers, self._client._session.headers) or {}
|
|
319
387
|
beta_features = [
|
|
320
388
|
value.strip()
|
|
321
389
|
for value in headers.get("x-codex-beta-features", "").split(",")
|
|
@@ -337,20 +405,21 @@ class Responses:
|
|
|
337
405
|
)
|
|
338
406
|
output: list[Any] = []
|
|
339
407
|
completed: dict[str, Any] = {}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
408
|
+
with stream_response_events(response) as events:
|
|
409
|
+
for event in events:
|
|
410
|
+
if event.type == "response.output_item.done":
|
|
411
|
+
item = getattr(event, "item", None)
|
|
412
|
+
if isinstance(item, dict):
|
|
413
|
+
output.append(item)
|
|
414
|
+
elif event.type == "response.completed":
|
|
415
|
+
completed_value = getattr(event, "response", None) or {}
|
|
416
|
+
completed = (
|
|
417
|
+
completed_value
|
|
418
|
+
if isinstance(completed_value, dict)
|
|
419
|
+
else completed_value.model_dump()
|
|
420
|
+
)
|
|
421
|
+
elif event.type in {"response.failed", "error"}:
|
|
422
|
+
raise RuntimeError(f"Remote compaction v2 failed: {_event_error_message(event)}")
|
|
354
423
|
|
|
355
424
|
compaction_items = [item for item in output if item.get("type") == "compaction"]
|
|
356
425
|
if len(compaction_items) != 1:
|
|
@@ -110,6 +110,12 @@ Supported backend fields are `input`, `model`, `instructions`, `include`,
|
|
|
110
110
|
iterator; otherwise the SDK collects the mandatory backend SSE stream into a
|
|
111
111
|
typed `Response`.
|
|
112
112
|
|
|
113
|
+
For HTTP `create`, `parse` and `compact` requests, a nonempty `prompt_cache_key`
|
|
114
|
+
also supplies the `session-id` header used by the ChatGPT backend for cache
|
|
115
|
+
affinity. An explicit `session-id` in `extra_headers` takes precedence
|
|
116
|
+
(case-insensitively). This preserves routing identity; it does not guarantee a
|
|
117
|
+
cache hit. WebSocket callers supply session headers at connection time.
|
|
118
|
+
|
|
113
119
|
The OpenAI-compatible signature also accepts the following names so it can
|
|
114
120
|
reject them clearly: `background`, `context_management`, `conversation`,
|
|
115
121
|
`max_output_tokens`, `max_tool_calls`, `metadata`, `previous_response_id`,
|
|
@@ -344,12 +344,18 @@ with client.live.sideband.connect(session_id=live.session.id) as sideband:
|
|
|
344
344
|
|
|
345
345
|
For Realtime v3, Codex sends `openai-alpha: quicksilver=v2`, the AVAS query
|
|
346
346
|
parameters `intent=quicksilver&architecture=avas`, and a `gpt-live` session.
|
|
347
|
-
|
|
347
|
+
Earlier WebRTC call-creation probes against the ChatGPT OAuth route accepted
|
|
348
348
|
`gpt-live-1-codex` and `gpt-live-1-boulder-alpha`: each returned an SDP answer
|
|
349
|
-
and a Realtime call id.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
and a Realtime call id. Some other identifiers returned HTTP 400 at that time.
|
|
350
|
+
The SDK does not encode these observations as an allowlist: `create_v3` forwards
|
|
351
|
+
model identifiers unchanged and preserves backend errors. Availability depends
|
|
352
|
+
on the account and backend rollout.
|
|
353
|
+
|
|
354
|
+
On October 22, 2026, a `gpt-live-1.1` probe returned an SDP answer and opened the
|
|
355
|
+
WebRTC data channel using a synthetic silent audio track (no microphone).
|
|
356
|
+
The server subsequently emitted `forbidden: Voice session access denied.`
|
|
357
|
+
No model reply was received; successful call creation is not proof of voice
|
|
358
|
+
session authorization.
|
|
353
359
|
|
|
354
360
|
This is distinct from the public Realtime API documented for developer API keys.
|
|
355
361
|
On the OpenAI API host, the same Codex implementation uses `/v1/live` for the
|