codex-backend-sdk 0.5.7__tar.gz → 0.5.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.5.7 → codex_backend_sdk-0.5.8}/CHANGELOG.md +15 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/PKG-INFO +19 -1
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/README.md +18 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/__init__.py +1 -1
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/_models.py +2 -2
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/_responses_payloads.py +26 -8
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_plugins.py +2 -4
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/responses_websocket.py +68 -7
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/docs/api-reference.md +17 -1
- codex_backend_sdk-0.5.8/docs/codex-source-audit.md +61 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/docs/desktop-endpoint-inventory.md +4 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/docs/endpoint-coverage.md +5 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/pyproject.toml +1 -1
- codex_backend_sdk-0.5.8/tests/general/test_codex_source_contracts.py +227 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/uv.lock +1 -1
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/.gitignore +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/LICENSE +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/_client.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/_codex_version.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/_transport.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/_utils.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/codex_client.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/_responses_routing.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_apps.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_connectors.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_plugin_bundles.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_plugin_shares.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_writing_blocks.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/codex.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/codex_web_search.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/images.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/models.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/openai_oauth.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/realtime.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/remote_control.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/responses.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/worktree_snapshots.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/docs/backend-api.md +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/docs/openai-compatibility.md +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/examples/agent.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_account_info.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_apps.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_connectors.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_plugin_bundles.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_plugin_shares.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_plugins.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_projects_gizmos.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_resources.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_search_files.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_chatgpt_writing_blocks.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_client_retry.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_codex_resources.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_codex_version.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_codex_web_search.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_hosted_mcp.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_images.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_oauth.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_openai_compatibility.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_openai_oauth_resources.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_prompt_cache_usage.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_realtime_resource.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_remote_control_resource.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_responses_resource.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_responses_routing.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_responses_websocket.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_structured_output.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_tools.py +0 -0
- {codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/tests/test_worktree_snapshots.py +0 -0
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.8] - 2026-10-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Responses WebSocket `interrupt()` with active-response tracking,
|
|
9
|
+
acknowledgement validation, and draining before socket reuse.
|
|
10
|
+
- Source compatibility audit against Codex `0eb98b4617`, with native cloud
|
|
11
|
+
gRPC Resume/Attach explicitly inventoried as unsupported.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Preserve message IDs, status, phase, encrypted content, and backend metadata
|
|
15
|
+
when replaying Responses history; retire abandoned WebSocket streams.
|
|
16
|
+
- Serialize numeric custom reasoning effort strings as unsigned 64-bit JSON
|
|
17
|
+
integers and preserve reasoning context on attribute-based controls.
|
|
18
|
+
- Send and validate installed-plugin pagination limits with scoped requests.
|
|
19
|
+
|
|
5
20
|
## [0.5.7] - 2026-10-22
|
|
6
21
|
|
|
7
22
|
### 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.8
|
|
4
4
|
Summary: Unofficial Python SDK for the ChatGPT Codex backend API
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -55,6 +55,12 @@ The package intentionally separates three kinds of API:
|
|
|
55
55
|
pip install --upgrade codex-backend-sdk
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
Version **0.5.8** aligns the SDK with Codex source commit `0eb98b4617`:
|
|
59
|
+
WebSocket interruption, message replay fields, numeric reasoning efforts, and
|
|
60
|
+
scoped installed-plugin pagination. See the [source audit](docs/codex-source-audit.md)
|
|
61
|
+
for verified contracts and the separately inventoried, unsupported cloud gRPC
|
|
62
|
+
transport.
|
|
63
|
+
|
|
58
64
|
For an editable checkout:
|
|
59
65
|
|
|
60
66
|
```bash
|
|
@@ -185,6 +191,18 @@ with client.responses.websocket.connect() as ws:
|
|
|
185
191
|
print(event.get("delta", ""), end="")
|
|
186
192
|
```
|
|
187
193
|
|
|
194
|
+
To stop an active WebSocket response, call `ws.interrupt()` after receiving
|
|
195
|
+
`response.created`, including from another thread. Keep consuming events until
|
|
196
|
+
both `response.interrupt.accepted` and the terminal response event have arrived
|
|
197
|
+
before reusing the connection. Closing an unfinished event iterator closes the
|
|
198
|
+
connection. Replayed messages retain their IDs, phase, status, encrypted content,
|
|
199
|
+
and backend metadata.
|
|
200
|
+
|
|
201
|
+
Custom numeric reasoning budgets are also supported:
|
|
202
|
+
`reasoning={"effort": 4096, "context": "all_turns"}`. Numeric strings such as
|
|
203
|
+
`"4096"` are encoded as JSON integers, matching Codex; model support is determined
|
|
204
|
+
by the backend.
|
|
205
|
+
|
|
188
206
|
### Maintain a multi-turn conversation
|
|
189
207
|
|
|
190
208
|
The Codex backend does not expose `previous_response_id`; preserve prior items
|
|
@@ -26,6 +26,12 @@ The package intentionally separates three kinds of API:
|
|
|
26
26
|
pip install --upgrade codex-backend-sdk
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
Version **0.5.8** aligns the SDK with Codex source commit `0eb98b4617`:
|
|
30
|
+
WebSocket interruption, message replay fields, numeric reasoning efforts, and
|
|
31
|
+
scoped installed-plugin pagination. See the [source audit](docs/codex-source-audit.md)
|
|
32
|
+
for verified contracts and the separately inventoried, unsupported cloud gRPC
|
|
33
|
+
transport.
|
|
34
|
+
|
|
29
35
|
For an editable checkout:
|
|
30
36
|
|
|
31
37
|
```bash
|
|
@@ -156,6 +162,18 @@ with client.responses.websocket.connect() as ws:
|
|
|
156
162
|
print(event.get("delta", ""), end="")
|
|
157
163
|
```
|
|
158
164
|
|
|
165
|
+
To stop an active WebSocket response, call `ws.interrupt()` after receiving
|
|
166
|
+
`response.created`, including from another thread. Keep consuming events until
|
|
167
|
+
both `response.interrupt.accepted` and the terminal response event have arrived
|
|
168
|
+
before reusing the connection. Closing an unfinished event iterator closes the
|
|
169
|
+
connection. Replayed messages retain their IDs, phase, status, encrypted content,
|
|
170
|
+
and backend metadata.
|
|
171
|
+
|
|
172
|
+
Custom numeric reasoning budgets are also supported:
|
|
173
|
+
`reasoning={"effort": 4096, "context": "all_turns"}`. Numeric strings such as
|
|
174
|
+
`"4096"` are encoded as JSON integers, matching Codex; model support is determined
|
|
175
|
+
by the backend.
|
|
176
|
+
|
|
159
177
|
### Maintain a multi-turn conversation
|
|
160
178
|
|
|
161
179
|
The Codex backend does not expose `previous_response_id`; preserve prior items
|
|
@@ -7,12 +7,12 @@ import time
|
|
|
7
7
|
from collections.abc import Iterator
|
|
8
8
|
from io import BytesIO
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Generic, Literal, Optional, TypeVar
|
|
10
|
+
from typing import Any, Generic, Literal, Optional, TypeVar, Union
|
|
11
11
|
|
|
12
12
|
import requests
|
|
13
13
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
14
14
|
|
|
15
|
-
ReasoningEffort = Literal["minimal", "low", "medium", "high", "xhigh"]
|
|
15
|
+
ReasoningEffort = Union[Literal["none", "minimal", "low", "medium", "high", "xhigh"], str, int]
|
|
16
16
|
ReasoningSummary = Literal["concise", "detailed", "auto"]
|
|
17
17
|
Verbosity = Literal["low", "medium", "high"]
|
|
18
18
|
ServiceTier = Literal["flex", "priority"]
|
|
@@ -16,7 +16,7 @@ from .._models import (
|
|
|
16
16
|
ResponseUsage,
|
|
17
17
|
TokenDetails,
|
|
18
18
|
)
|
|
19
|
-
from .._utils import
|
|
19
|
+
from .._utils import _default, _is_given, _jsonable
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ResponsesCreateRequest(CodexBaseModel):
|
|
@@ -169,7 +169,15 @@ def normalize_input_item(item: Any) -> dict[str, Any]:
|
|
|
169
169
|
{"type": "input_text", "text": part} if isinstance(part, str) else part
|
|
170
170
|
for part in content
|
|
171
171
|
]
|
|
172
|
-
|
|
172
|
+
message = _message(role, content)
|
|
173
|
+
# Preserve backend replay fields while still excluding local harness-only keys.
|
|
174
|
+
for key in (
|
|
175
|
+
"id", "status", "phase", "encrypted_content",
|
|
176
|
+
"internal_chat_message_metadata_passthrough",
|
|
177
|
+
):
|
|
178
|
+
if key in raw:
|
|
179
|
+
message[key] = raw[key]
|
|
180
|
+
return message
|
|
173
181
|
|
|
174
182
|
|
|
175
183
|
def normalize_tools(tools: Any) -> list[dict[str, Any]]:
|
|
@@ -181,12 +189,22 @@ def normalize_tools(tools: Any) -> list[dict[str, Any]]:
|
|
|
181
189
|
def normalize_reasoning(reasoning: Any) -> dict[str, Any]:
|
|
182
190
|
reasoning = _jsonable(reasoning)
|
|
183
191
|
if isinstance(reasoning, dict):
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
normalized = {key: value for key, value in reasoning.items() if value is not None}
|
|
193
|
+
else:
|
|
194
|
+
normalized = {
|
|
195
|
+
key: value
|
|
196
|
+
for key in ("effort", "summary", "context")
|
|
197
|
+
if (value := getattr(reasoning, key, None)) is not None
|
|
198
|
+
}
|
|
199
|
+
effort = normalized.get("effort")
|
|
200
|
+
# Codex serializes numeric custom efforts as unsigned 64-bit JSON numbers.
|
|
201
|
+
if isinstance(effort, str) and effort.isascii():
|
|
202
|
+
digits = effort.removeprefix("+")
|
|
203
|
+
if digits.isdecimal() and len(digits.lstrip("0")) <= 20:
|
|
204
|
+
numeric = int(digits.lstrip("0") or "0")
|
|
205
|
+
if numeric <= 2**64 - 1:
|
|
206
|
+
normalized["effort"] = numeric
|
|
207
|
+
return normalized
|
|
190
208
|
|
|
191
209
|
|
|
192
210
|
def normalize_text(text: Any) -> dict[str, Any]:
|
{codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_plugins.py
RENAMED
|
@@ -106,10 +106,8 @@ class ChatGPTPlugins:
|
|
|
106
106
|
page_token: str | None = None,
|
|
107
107
|
include_download_urls: bool = False,
|
|
108
108
|
) -> dict[str, Any]:
|
|
109
|
-
params: dict[str, Any] = {}
|
|
110
|
-
if scope is None:
|
|
111
|
-
params["limit"] = _positive(limit, "limit")
|
|
112
|
-
else:
|
|
109
|
+
params: dict[str, Any] = {"limit": _positive(limit, "limit")}
|
|
110
|
+
if scope is not None:
|
|
113
111
|
params["scope"] = _scope(scope)
|
|
114
112
|
if page_token is not None:
|
|
115
113
|
params["pageToken"] = _required(page_token, "page_token")
|
|
@@ -38,6 +38,10 @@ class ResponsesWebSocketConnection:
|
|
|
38
38
|
self._socket = socket
|
|
39
39
|
self.url = url
|
|
40
40
|
self._request_lock = threading.Lock()
|
|
41
|
+
self._state_lock = threading.RLock()
|
|
42
|
+
self._response_id: str | None = None
|
|
43
|
+
self._interrupt_requested = False
|
|
44
|
+
self._interrupt_pending = False
|
|
41
45
|
self.closed = False
|
|
42
46
|
headers = _handshake_headers(socket)
|
|
43
47
|
self.reasoning_included = "x-reasoning-included" in headers
|
|
@@ -68,32 +72,59 @@ class ResponsesWebSocketConnection:
|
|
|
68
72
|
raise RuntimeError(
|
|
69
73
|
"Responses WebSocket connection already has an active response."
|
|
70
74
|
)
|
|
75
|
+
finished = False
|
|
71
76
|
try:
|
|
72
|
-
self.
|
|
77
|
+
with self._state_lock:
|
|
78
|
+
self._response_id = None
|
|
79
|
+
self._interrupt_requested = False
|
|
80
|
+
self._interrupt_pending = False
|
|
81
|
+
self._socket.send(encoded)
|
|
82
|
+
terminal = False
|
|
73
83
|
while True:
|
|
74
84
|
raw = self._socket.recv()
|
|
75
85
|
if raw in {None, "", b""}:
|
|
76
|
-
self.closed = True
|
|
77
86
|
raise RuntimeError(
|
|
78
87
|
"Responses WebSocket closed before a terminal response event."
|
|
79
88
|
)
|
|
80
89
|
if isinstance(raw, bytes):
|
|
81
|
-
self.closed = True
|
|
82
90
|
raise RuntimeError("Responses WebSocket returned unexpected binary data.")
|
|
83
91
|
try:
|
|
84
92
|
event = json.loads(raw)
|
|
85
93
|
except (TypeError, json.JSONDecodeError) as exc:
|
|
86
|
-
self.closed = True
|
|
87
94
|
raise RuntimeError("Responses WebSocket returned invalid JSON.") from exc
|
|
88
95
|
if not isinstance(event, dict) or not isinstance(event.get("type"), str):
|
|
89
|
-
self.closed = True
|
|
90
96
|
raise RuntimeError(
|
|
91
97
|
"Responses WebSocket returned an invalid event envelope."
|
|
92
98
|
)
|
|
93
99
|
if event["type"] == "error":
|
|
94
100
|
raise _event_error(event)
|
|
101
|
+
with self._state_lock:
|
|
102
|
+
if event["type"] == "response.created":
|
|
103
|
+
response = event.get("response")
|
|
104
|
+
response_id = response.get("id") if isinstance(response, dict) else None
|
|
105
|
+
if isinstance(response_id, str) and response_id:
|
|
106
|
+
self._response_id = response_id
|
|
107
|
+
elif event["type"] in {
|
|
108
|
+
"response.interrupt.accepted", "response.interrupt.failed"
|
|
109
|
+
}:
|
|
110
|
+
if (
|
|
111
|
+
not self._interrupt_pending
|
|
112
|
+
or event.get("response_id") != self._response_id
|
|
113
|
+
):
|
|
114
|
+
raise RuntimeError("Unexpected response interruption acknowledgement.")
|
|
115
|
+
if event["type"] == "response.interrupt.failed":
|
|
116
|
+
raise RuntimeError("Response interruption was rejected.")
|
|
117
|
+
self._interrupt_pending = False
|
|
118
|
+
if event["type"] in _TERMINAL_EVENTS:
|
|
119
|
+
terminal = True
|
|
120
|
+
# Prevent a late interrupt after completion while draining its ack.
|
|
121
|
+
self._interrupt_requested = True
|
|
122
|
+
done = terminal and not self._interrupt_pending
|
|
123
|
+
if done:
|
|
124
|
+
self._response_id = None
|
|
125
|
+
finished = True
|
|
95
126
|
yield event
|
|
96
|
-
if
|
|
127
|
+
if done:
|
|
97
128
|
return
|
|
98
129
|
except Exception:
|
|
99
130
|
if not self.closed:
|
|
@@ -103,7 +134,37 @@ class ResponsesWebSocketConnection:
|
|
|
103
134
|
self.closed = True
|
|
104
135
|
raise
|
|
105
136
|
finally:
|
|
106
|
-
|
|
137
|
+
try:
|
|
138
|
+
with self._state_lock:
|
|
139
|
+
self._response_id = None
|
|
140
|
+
if not finished:
|
|
141
|
+
self.close()
|
|
142
|
+
finally:
|
|
143
|
+
self._request_lock.release()
|
|
144
|
+
|
|
145
|
+
def interrupt(self) -> None:
|
|
146
|
+
"""Interrupt the active response, discarding partial items.
|
|
147
|
+
|
|
148
|
+
Call after ``response.created``, including from another thread while
|
|
149
|
+
reading events. Continue consuming through both the acknowledgement
|
|
150
|
+
and terminal response event before reusing the connection.
|
|
151
|
+
"""
|
|
152
|
+
with self._state_lock:
|
|
153
|
+
if self.closed:
|
|
154
|
+
raise RuntimeError("Responses WebSocket connection is closed.")
|
|
155
|
+
if self._response_id is None or self._interrupt_requested:
|
|
156
|
+
raise RuntimeError("No active response is available to interrupt.")
|
|
157
|
+
self._interrupt_requested = True
|
|
158
|
+
self._interrupt_pending = True
|
|
159
|
+
try:
|
|
160
|
+
self._socket.send(json.dumps({
|
|
161
|
+
"type": "response.interrupt",
|
|
162
|
+
"response_id": self._response_id,
|
|
163
|
+
"mode": "discard_partial_items",
|
|
164
|
+
}, separators=(",", ":")))
|
|
165
|
+
except Exception:
|
|
166
|
+
self.close()
|
|
167
|
+
raise
|
|
107
168
|
|
|
108
169
|
def create(self, request: Mapping[str, Any]) -> Iterator[dict[str, Any]]:
|
|
109
170
|
"""Alias for :meth:`events` using Responses resource terminology."""
|
|
@@ -110,6 +110,13 @@ 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
|
+
`reasoning` dictionaries preserve additive backend controls such as
|
|
114
|
+
`context="all_turns"`. Numeric custom effort strings (for example `"4096"`)
|
|
115
|
+
are serialized as unsigned 64-bit JSON integers, matching Codex; named efforts
|
|
116
|
+
remain strings. This also applies to compaction. Message history retains
|
|
117
|
+
backend replay fields (`id`, `status`, `phase`, `encrypted_content`, and
|
|
118
|
+
`internal_chat_message_metadata_passthrough`) and content-part metadata.
|
|
119
|
+
|
|
113
120
|
For HTTP `create`, `parse` and `compact` requests, a nonempty `prompt_cache_key`
|
|
114
121
|
also supplies the `session-id` header used by the ChatGPT backend for cache
|
|
115
122
|
affinity. An explicit `session-id` in `extra_headers` takes precedence
|
|
@@ -165,11 +172,20 @@ Connection methods:
|
|
|
165
172
|
| --- | --- | --- |
|
|
166
173
|
| `create(request)` | raw Responses request mapping | iterator of raw event dictionaries |
|
|
167
174
|
| `events(request)` | raw `response.create` event or request mapping | iterator through terminal completion/error |
|
|
175
|
+
| `interrupt()` | none; active response must have emitted `response.created` | sends `response.interrupt` with `discard_partial_items` |
|
|
168
176
|
| `close()` | none | `None` |
|
|
169
177
|
|
|
170
178
|
The connection is a context manager and is reusable for sequential, not
|
|
171
179
|
concurrent, turns. Backend error envelopes raise `ResponsesWebSocketError` with
|
|
172
|
-
`code`,
|
|
180
|
+
`code`, the exception message, `event`, and optional `status`.
|
|
181
|
+
|
|
182
|
+
After `interrupt()`, keep consuming events through both
|
|
183
|
+
`response.interrupt.accepted` and the terminal response event. They may arrive
|
|
184
|
+
in either order. The acknowledgement must match the active response ID;
|
|
185
|
+
rejection or an invalid acknowledgement closes the connection. You can call
|
|
186
|
+
`interrupt()` from another thread while consuming the stream. Abandoning an
|
|
187
|
+
unfinished event iterator also closes the connection to prevent unread events
|
|
188
|
+
from leaking into the next request.
|
|
173
189
|
|
|
174
190
|
### Models
|
|
175
191
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Codex source compatibility audit — 2026-10-22
|
|
2
|
+
|
|
3
|
+
Compared `openai/codex` from the previously audited
|
|
4
|
+
`7498521d288b9b3b96ffba4eedf089d8d6e06a84` to
|
|
5
|
+
`0eb98b46176fd41fc285e5e2f82218bf0ccd70f5` (`origin/main`). The local checkout
|
|
6
|
+
was updated by fast-forward and has no local changes. This is a source-contract
|
|
7
|
+
audit; the packaged Desktop ASAR was not rescanned and no live backend requests
|
|
8
|
+
were used to validate the new contracts.
|
|
9
|
+
|
|
10
|
+
## Scope and method
|
|
11
|
+
|
|
12
|
+
Reviewed changed production contracts in `codex-rs/codex-api`, `backend-client`,
|
|
13
|
+
`core-plugins` remote catalog/search/sharing, `chatgpt` connectors,
|
|
14
|
+
`app-server-transport` Remote Control, `login`, cloud tasks, and `cloud-client`.
|
|
15
|
+
Searched changed Rust production sources across `codex-rs` for added backend
|
|
16
|
+
route literals and checked request builders, response types, and protocol item
|
|
17
|
+
changes. Local app-server JSON-RPC methods, CLI flags, and harness policy are
|
|
18
|
+
outside this HTTP backend SDK's scope.
|
|
19
|
+
|
|
20
|
+
## Findings and disposition
|
|
21
|
+
|
|
22
|
+
| Upstream change | SDK disposition and verification |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| WebSocket `response.interrupt` with `response_id` and `mode: discard_partial_items` | Added `connection.interrupt()`. Tracks the active response, checks the acknowledgement ID, rejects failed/unsolicited acknowledgements, and drains both acknowledgement and terminal event before reuse. Closing an unfinished iterator retires the socket. Tests cover cross-thread interruption and completion arriving before acknowledgement. |
|
|
25
|
+
| Numeric custom reasoning effort serialized as an unsigned 64-bit JSON number | HTTP Responses and compaction convert parseable numeric effort strings into integers, matching Codex. Existing numbers, named/custom string efforts, and additive dictionary options remain supported. Attribute-based controls retain `context`. Exported `ReasoningEffort` accepts strings and integers. |
|
|
26
|
+
| Messages gain `encrypted_content` and `status`; `phase` gains `partial_answer`; output text gains `annotations`/`logprobs` | Message normalization now retains known replay fields, including `id`, `phase`, and metadata. Content parts and non-message items remain raw. A round-trip test protects opaque fields, empty arrays, `additional_tools`, and `configuration_update`. Local `kind`/`call_id` keys on ordinary messages remain filtered. |
|
|
27
|
+
| Installed-plugin pagination always sends `limit`, including scoped requests | Fixed `.plugins.installed(scope=..., limit=...)`; validates and transmits the limit alongside the scope and cursor. Aggregation already rejects repeated cursors. |
|
|
28
|
+
| Image edit references allow both `image_url` and `file_id` | Already supported; mixed references are contract-tested. HTTP failures retain the underlying response and headers, including nested ImageGen request IDs. |
|
|
29
|
+
| Thread usage and groups gain `native_usage_usd_micros`; plugin release gains `onboarding_skill_name`; new plan names | Existing raw dictionaries preserve additive fields. Contract test distinguishes absent/null/zero/native amounts without estimating them. |
|
|
30
|
+
| Base instructions move from the top-level field into developer input messages (`c9253c4977`) | Developer messages are supported, and their explicit IDs/metadata now survive normalization. The OpenAI-shaped `instructions=` argument keeps its existing top-level encoding for compatibility; removing it from Codex's own request struct does not establish backend rejection. No claim of live equivalence or retirement of the old field is made. |
|
|
31
|
+
| Responses serializes `model`, `stream`, `service_tier` before large input (`ed0cc1a4ab`) | Values already supported; ordering is an upstream gateway optimization, not a newly required parameter. |
|
|
32
|
+
| Provider-specific model catalog URLs and plugin product SKU become configurable | Provider/gateway configuration is outside the SDK's fixed ChatGPT backend routing. Catalog query overrides and plugin `codex` product headers remain supported. |
|
|
33
|
+
| Realtime transcript handling, shared HTTP/network policy, proxy fallback, file PUT retries, OAuth storage/provenance changes | Existing endpoint paths and request schemas remain compatible. These client implementation/policy changes do not add required backend parameters. Realtime events and WebSocket envelopes remain raw. |
|
|
34
|
+
| Native cloud ThreadService `Resume` and live `Attach` | **Inventory, unsupported.** Separate HTTP/2 gRPC/protobuf transport, not an HTTP JSON endpoint or app-server JSON-RPC. Upstream requires a caller-supplied trusted origin and explicitly does not establish production ingress. Do not guess a URL or send these payloads through the SDK's JSON transport. |
|
|
35
|
+
|
|
36
|
+
Relevant source locations at the audited commit:
|
|
37
|
+
|
|
38
|
+
- `codex-rs/codex-api/src/common.rs`
|
|
39
|
+
- `codex-rs/codex-api/src/endpoint/responses_websocket.rs`
|
|
40
|
+
- `codex-rs/core/src/client.rs` and `core/src/context/base_instructions.rs`
|
|
41
|
+
- `codex-rs/protocol/src/models.rs`
|
|
42
|
+
- `codex-rs/core-plugins/src/remote.rs`
|
|
43
|
+
- `codex-rs/backend-client/src/client/thread_usage.rs`
|
|
44
|
+
- `codex-rs/cloud-client/README.md`, `src/lib.rs`, and `src/wire.rs`
|
|
45
|
+
|
|
46
|
+
No new HTTP route in the reviewed backend families requires a new SDK resource.
|
|
47
|
+
The native gRPC surface is explicitly tracked above. Earlier Desktop-only
|
|
48
|
+
coverage and exclusions remain as recorded in `desktop-endpoint-inventory.md`.
|
|
49
|
+
|
|
50
|
+
## Validation
|
|
51
|
+
|
|
52
|
+
Durable behavioral contracts added by this audit live in
|
|
53
|
+
`tests/general/test_codex_source_contracts.py`. Existing local contract tests
|
|
54
|
+
are also run; tests using the authenticated `client` fixture are excluded from
|
|
55
|
+
the local-only run. No transient diagnostic tests are retained in
|
|
56
|
+
`tests/agentic/`, and no backend mutation or inference is performed by these tests.
|
|
57
|
+
|
|
58
|
+
Result: **310 passed, 12 live tests deselected**. Ruff's correctness rules
|
|
59
|
+
(`E4`, `E7`, `E9`, `F`, Python 3.9 target) and `git diff --check` also pass for
|
|
60
|
+
the changed Python files. The broader Ruff style rules report existing issues
|
|
61
|
+
in these modules and are not a clean project baseline.
|
|
@@ -5,6 +5,10 @@ routes referenced by the official Codex Desktop application but not necessarily
|
|
|
5
5
|
by `codex-rs`. It is an implementation survey, not a stability guarantee or an
|
|
6
6
|
assertion that every route is enabled for every account.
|
|
7
7
|
|
|
8
|
+
For the later Codex checkout update through `0eb98b4617`, see the
|
|
9
|
+
[2026-10-22 source audit](codex-source-audit.md). That audit does not replace or
|
|
10
|
+
refresh the packaged Desktop snapshot recorded below.
|
|
11
|
+
|
|
8
12
|
## Audited snapshot and method
|
|
9
13
|
|
|
10
14
|
- Codex checkout: `openai/codex` at
|
|
@@ -4,6 +4,11 @@ This is the living implementation matrix for backend surfaces found in the
|
|
|
4
4
|
official Codex and Codex Desktop clients. The source snapshot and discovery
|
|
5
5
|
method are recorded in `desktop-endpoint-inventory.md`.
|
|
6
6
|
|
|
7
|
+
The subsequent [2026-10-22 Codex source audit](codex-source-audit.md) records
|
|
8
|
+
compatibility through `0eb98b46176fd41fc285e5e2f82218bf0ccd70f5`, including
|
|
9
|
+
WebSocket interruption, message replay fields, numeric reasoning, scoped plugin
|
|
10
|
+
pagination, and the separately inventoried native cloud gRPC transport.
|
|
11
|
+
|
|
7
12
|
Statuses:
|
|
8
13
|
|
|
9
14
|
- **live**: exercised against the backend with Codex OAuth;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Durable wire guarantees for evolving Codex backend contracts."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from queue import Queue
|
|
6
|
+
from threading import Event
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from codex_backend_sdk import OpenAI, ResponsesWebSocketConnection
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CapturingClient(OpenAI):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__(model="test-model")
|
|
17
|
+
self.calls = []
|
|
18
|
+
|
|
19
|
+
def _post(self, path, **kwargs):
|
|
20
|
+
self.calls.append((path, kwargs))
|
|
21
|
+
return SimpleNamespace(
|
|
22
|
+
iter_lines=lambda: iter([
|
|
23
|
+
b'data: {"type":"response.completed","response":{"id":"r1"}}', b""
|
|
24
|
+
]),
|
|
25
|
+
close=lambda: None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def _post_raw(self, path, **kwargs):
|
|
29
|
+
self.calls.append((path, kwargs))
|
|
30
|
+
return SimpleNamespace(json=lambda: {"created": 1, "data": []})
|
|
31
|
+
|
|
32
|
+
def _get_chatgpt(self, path, **kwargs):
|
|
33
|
+
self.calls.append((path, kwargs))
|
|
34
|
+
return {"plugins": [], "pagination": {"nextPageToken": None}}
|
|
35
|
+
|
|
36
|
+
def _post_wham(self, path, **kwargs):
|
|
37
|
+
self.calls.append((path, kwargs))
|
|
38
|
+
return {"threads": [{
|
|
39
|
+
"thread_id": "t1", "native_usage_usd_micros": None,
|
|
40
|
+
"groups": [{}, {"native_usage_usd_micros": 0}, {"native_usage_usd_micros": 250}],
|
|
41
|
+
}]}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("effort,expected", [
|
|
45
|
+
(4096, 4096), ("4096", 4096), ("+4096", 4096), ("000", 0),
|
|
46
|
+
("18446744073709551615", 2**64 - 1),
|
|
47
|
+
("18446744073709551616", "18446744073709551616"),
|
|
48
|
+
("high", "high"), ("custom-budget", "custom-budget"), ("-1", "-1"),
|
|
49
|
+
])
|
|
50
|
+
@pytest.mark.parametrize("compact", [False, True])
|
|
51
|
+
def test_numeric_reasoning_and_context_reach_both_http_transports(effort, expected, compact):
|
|
52
|
+
client = CapturingClient()
|
|
53
|
+
reasoning = {"effort": effort, "context": "all_turns", "future_option": True}
|
|
54
|
+
if compact:
|
|
55
|
+
# Compaction needs an opaque result; inspect the request even if the fixture has none.
|
|
56
|
+
with pytest.raises(RuntimeError):
|
|
57
|
+
client.responses.compact(input=[], reasoning=reasoning)
|
|
58
|
+
else:
|
|
59
|
+
client.responses.create(input="hello", reasoning=reasoning)
|
|
60
|
+
assert client.calls[0][1]["body"]["reasoning"] == {
|
|
61
|
+
"effort": expected, "context": "all_turns", "future_option": True,
|
|
62
|
+
}
|
|
63
|
+
assert reasoning["effort"] == effort
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_reasoning_attribute_objects_preserve_context():
|
|
67
|
+
client = CapturingClient()
|
|
68
|
+
client.responses.create(
|
|
69
|
+
input="hello", reasoning=SimpleNamespace(effort="8192", context="current_turn")
|
|
70
|
+
)
|
|
71
|
+
assert client.calls[0][1]["body"]["reasoning"] == {
|
|
72
|
+
"effort": 8192, "context": "current_turn",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_response_replay_preserves_opaque_message_fields_and_new_item_types():
|
|
77
|
+
client = CapturingClient()
|
|
78
|
+
items = [
|
|
79
|
+
{"type": "message", "id": "m1", "role": "developer", "content": [],
|
|
80
|
+
"internal_chat_message_metadata_passthrough": {
|
|
81
|
+
"content_item_kinds": ["model.base_instructions"],
|
|
82
|
+
}},
|
|
83
|
+
{"type": "message", "id": "m2", "role": "assistant", "status": "completed",
|
|
84
|
+
"phase": "partial_answer", "encrypted_content": "opaque", "content": [
|
|
85
|
+
{"type": "output_text", "text": "hello", "annotations": [], "logprobs": []},
|
|
86
|
+
]},
|
|
87
|
+
{"type": "additional_tools", "role": "developer", "tools": []},
|
|
88
|
+
{"type": "configuration_update", "reasoning": {"effort": 4096}},
|
|
89
|
+
]
|
|
90
|
+
client.responses.create(input=items)
|
|
91
|
+
assert client.calls[0][1]["body"]["input"] == items
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_installed_plugins_scope_keeps_limit_and_cursor():
|
|
95
|
+
client = CapturingClient()
|
|
96
|
+
client.chatgpt.plugins.installed(scope="WORKSPACE", limit=7, page_token="next")
|
|
97
|
+
assert client.calls[0][1]["params"] == {
|
|
98
|
+
"scope": "WORKSPACE", "limit": 7, "pageToken": "next",
|
|
99
|
+
}
|
|
100
|
+
with pytest.raises(ValueError):
|
|
101
|
+
client.chatgpt.plugins.installed(scope="WORKSPACE", limit=0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_image_file_references_and_native_usage_are_preserved():
|
|
105
|
+
client = CapturingClient()
|
|
106
|
+
images = [{"image_url": "data:image/png;base64,Zm9v"}, {"file_id": "file-image"}]
|
|
107
|
+
client.images.edit(image=images, prompt="add a red hat")
|
|
108
|
+
assert client.calls[0][1]["body"]["images"] == images
|
|
109
|
+
assert client.codex.usage_details.threads(["t1"]) == {"threads": [{
|
|
110
|
+
"thread_id": "t1", "native_usage_usd_micros": None,
|
|
111
|
+
"groups": [{}, {"native_usage_usd_micros": 0}, {"native_usage_usd_micros": 250}],
|
|
112
|
+
}]}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Socket:
|
|
116
|
+
def __init__(self, events):
|
|
117
|
+
self.events = iter(events)
|
|
118
|
+
self.sent = []
|
|
119
|
+
self.closed = False
|
|
120
|
+
|
|
121
|
+
def send(self, message):
|
|
122
|
+
self.sent.append(json.loads(message))
|
|
123
|
+
|
|
124
|
+
def recv(self):
|
|
125
|
+
event = next(self.events, None)
|
|
126
|
+
return "" if event is None else json.dumps(event)
|
|
127
|
+
|
|
128
|
+
def close(self):
|
|
129
|
+
self.closed = True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.parametrize("ack_first", [True, False])
|
|
133
|
+
def test_interrupt_drains_ack_and_completion_before_socket_reuse(ack_first):
|
|
134
|
+
ack = {"type": "response.interrupt.accepted", "response_id": "r1"}
|
|
135
|
+
terminal = {"type": "response.completed", "response": {"id": "r1"}}
|
|
136
|
+
socket = Socket([
|
|
137
|
+
{"type": "response.created", "response": {"id": "r1"}},
|
|
138
|
+
*([ack, terminal] if ack_first else [terminal, ack]),
|
|
139
|
+
{"type": "response.completed", "response": {"id": "r2"}},
|
|
140
|
+
])
|
|
141
|
+
connection = ResponsesWebSocketConnection(socket, url="wss://example.test")
|
|
142
|
+
with pytest.raises(RuntimeError, match="No active"):
|
|
143
|
+
connection.interrupt()
|
|
144
|
+
stream = connection.events({"model": "test-model"})
|
|
145
|
+
next(stream)
|
|
146
|
+
connection.interrupt()
|
|
147
|
+
assert socket.sent[-1] == {
|
|
148
|
+
"type": "response.interrupt", "response_id": "r1", "mode": "discard_partial_items",
|
|
149
|
+
}
|
|
150
|
+
with pytest.raises(RuntimeError, match="No active"):
|
|
151
|
+
connection.interrupt()
|
|
152
|
+
assert len(list(stream)) == 2
|
|
153
|
+
second = list(connection.events({"model": "test-model"}))
|
|
154
|
+
assert second[0]["response"]["id"] == "r2"
|
|
155
|
+
assert not socket.closed
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.parametrize("ack", [
|
|
159
|
+
{"type": "response.interrupt.accepted", "response_id": "wrong"},
|
|
160
|
+
{"type": "response.interrupt.failed", "response_id": "r1"},
|
|
161
|
+
{"type": "response.interrupt.accepted"},
|
|
162
|
+
])
|
|
163
|
+
def test_invalid_interrupt_ack_retires_socket(ack):
|
|
164
|
+
socket = Socket([{"type": "response.created", "response": {"id": "r1"}}, ack])
|
|
165
|
+
connection = ResponsesWebSocketConnection(socket, url="wss://example.test")
|
|
166
|
+
stream = connection.events({"model": "test-model"})
|
|
167
|
+
next(stream)
|
|
168
|
+
connection.interrupt()
|
|
169
|
+
with pytest.raises(RuntimeError, match="interruption"):
|
|
170
|
+
list(stream)
|
|
171
|
+
assert socket.closed and connection.closed
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_abandoned_stream_cannot_reuse_socket_with_unread_events():
|
|
175
|
+
socket = Socket([{"type": "response.created", "response": {"id": "r1"}}])
|
|
176
|
+
connection = ResponsesWebSocketConnection(socket, url="wss://example.test")
|
|
177
|
+
stream = connection.events({"model": "test-model"})
|
|
178
|
+
next(stream)
|
|
179
|
+
stream.close()
|
|
180
|
+
assert socket.closed and connection.closed
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@pytest.mark.parametrize("remaining", [
|
|
184
|
+
[],
|
|
185
|
+
[{"type": "response.completed", "response": {"id": "r1"}}],
|
|
186
|
+
[{"type": "response.interrupt.accepted", "response_id": "r1"}],
|
|
187
|
+
])
|
|
188
|
+
def test_incomplete_interrupt_drain_closes_socket(remaining):
|
|
189
|
+
socket = Socket([{"type": "response.created", "response": {"id": "r1"}}, *remaining])
|
|
190
|
+
connection = ResponsesWebSocketConnection(socket, url="wss://example.test")
|
|
191
|
+
stream = connection.events({"model": "test-model"})
|
|
192
|
+
next(stream)
|
|
193
|
+
connection.interrupt()
|
|
194
|
+
with pytest.raises(RuntimeError, match="closed before"):
|
|
195
|
+
list(stream)
|
|
196
|
+
assert socket.closed and connection.closed
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_interrupt_from_another_thread_while_receiving():
|
|
200
|
+
ready = Event()
|
|
201
|
+
incoming = Queue()
|
|
202
|
+
incoming.put({"type": "response.created", "response": {"id": "r1"}})
|
|
203
|
+
|
|
204
|
+
class BlockingSocket(Socket):
|
|
205
|
+
def recv(self):
|
|
206
|
+
if len(self.sent) == 1 and incoming.empty():
|
|
207
|
+
ready.set()
|
|
208
|
+
return json.dumps(incoming.get(timeout=3))
|
|
209
|
+
|
|
210
|
+
def send(self, message):
|
|
211
|
+
super().send(message)
|
|
212
|
+
if self.sent[-1]["type"] == "response.interrupt":
|
|
213
|
+
incoming.put({"type": "response.interrupt.accepted", "response_id": "r1"})
|
|
214
|
+
incoming.put({"type": "response.completed", "response": {"id": "r1"}})
|
|
215
|
+
|
|
216
|
+
socket = BlockingSocket([])
|
|
217
|
+
connection = ResponsesWebSocketConnection(socket, url="wss://example.test")
|
|
218
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
219
|
+
result = executor.submit(lambda: list(connection.events({"model": "test-model"})))
|
|
220
|
+
try:
|
|
221
|
+
assert ready.wait(timeout=3)
|
|
222
|
+
connection.interrupt()
|
|
223
|
+
assert [event["type"] for event in result.result(timeout=3)] == [
|
|
224
|
+
"response.created", "response.interrupt.accepted", "response.completed",
|
|
225
|
+
]
|
|
226
|
+
finally:
|
|
227
|
+
connection.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/chatgpt_apps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/codex_web_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/openai_oauth.py
RENAMED
|
File without changes
|
|
File without changes
|
{codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/remote_control.py
RENAMED
|
File without changes
|
{codex_backend_sdk-0.5.7 → codex_backend_sdk-0.5.8}/codex_backend_sdk/resources/responses.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|