codex-backend-sdk 0.5.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/CHANGELOG.md +6 -0
  2. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/PKG-INFO +1 -1
  3. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/__init__.py +3 -1
  4. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/remote_control.py +12 -0
  5. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/responses.py +3 -3
  6. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/docs/api-reference.md +7 -4
  7. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/docs/backend-api.md +3 -1
  8. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/pyproject.toml +1 -1
  9. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_remote_control_resource.py +29 -1
  10. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_responses_resource.py +37 -0
  11. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/uv.lock +1 -1
  12. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/.gitignore +0 -0
  13. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/LICENSE +0 -0
  14. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/README.md +0 -0
  15. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/_client.py +0 -0
  16. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/_models.py +0 -0
  17. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/_streaming.py +0 -0
  18. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/_transport.py +0 -0
  19. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/_utils.py +0 -0
  20. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/codex_client.py +0 -0
  21. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/oauth.py +0 -0
  22. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/pkce.py +0 -0
  23. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/__init__.py +0 -0
  24. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
  25. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/chatgpt.py +0 -0
  26. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/chatgpt_apps.py +0 -0
  27. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/chatgpt_connectors.py +0 -0
  28. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/chatgpt_plugin_bundles.py +0 -0
  29. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/chatgpt_plugin_shares.py +0 -0
  30. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/chatgpt_plugins.py +0 -0
  31. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/chatgpt_writing_blocks.py +0 -0
  32. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/codex.py +0 -0
  33. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/codex_web_search.py +0 -0
  34. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/files.py +0 -0
  35. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/images.py +0 -0
  36. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/models.py +0 -0
  37. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/openai_oauth.py +0 -0
  38. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/realtime.py +0 -0
  39. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/responses_websocket.py +0 -0
  40. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/resources/worktree_snapshots.py +0 -0
  41. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/codex_backend_sdk/storage.py +0 -0
  42. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/docs/desktop-endpoint-inventory.md +0 -0
  43. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/docs/endpoint-coverage.md +0 -0
  44. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/docs/openai-compatibility.md +0 -0
  45. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/examples/agent.py +0 -0
  46. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/conftest.py +0 -0
  47. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_account_info.py +0 -0
  48. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_basic.py +0 -0
  49. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_apps.py +0 -0
  50. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_connectors.py +0 -0
  51. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_plugin_bundles.py +0 -0
  52. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_plugin_shares.py +0 -0
  53. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_plugins.py +0 -0
  54. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_projects_gizmos.py +0 -0
  55. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_resources.py +0 -0
  56. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_search_files.py +0 -0
  57. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_chatgpt_writing_blocks.py +0 -0
  58. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_client_retry.py +0 -0
  59. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_codex_resources.py +0 -0
  60. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_codex_web_search.py +0 -0
  61. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_conversation.py +0 -0
  62. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_files_resource.py +0 -0
  63. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_hosted_mcp.py +0 -0
  64. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_images.py +0 -0
  65. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_oauth.py +0 -0
  66. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_openai_compatibility.py +0 -0
  67. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_openai_oauth_resources.py +0 -0
  68. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_prompt_cache_usage.py +0 -0
  69. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_realtime_resource.py +0 -0
  70. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_reasoning.py +0 -0
  71. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_responses_websocket.py +0 -0
  72. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_structured_output.py +0 -0
  73. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_tools.py +0 -0
  74. {codex_backend_sdk-0.5.0 → codex_backend_sdk-0.5.2}/tests/test_worktree_snapshots.py +0 -0
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.5.1] - 2026-08-14
8
+
9
+ ### Fixed
10
+ - Preserve ordered nonopaque remote-compaction output items instead of rejecting
11
+ any response containing more than the required opaque compaction anchor.
12
+
7
13
  ## [0.5.0] - 2026-08-13
8
14
 
9
15
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codex-backend-sdk
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -17,7 +17,7 @@ Quickstart:
17
17
  print(response.output_text)
18
18
  """
19
19
 
20
- __version__ = "0.5.0"
20
+ __version__ = "0.5.2"
21
21
 
22
22
  from .oauth import (
23
23
  DeviceCode,
@@ -32,6 +32,7 @@ from .resources.remote_control import (
32
32
  RemoteControlClient,
33
33
  RemoteControlClientPage,
34
34
  RemoteControlConnection,
35
+ RemoteControlConnectionClosed,
35
36
  RemoteControlDesktop,
36
37
  RemoteControlEnrollment,
37
38
  RemoteControlEnvironment,
@@ -120,6 +121,7 @@ __all__ = [
120
121
  "RemoteControlClient",
121
122
  "RemoteControlClientPage",
122
123
  "RemoteControlConnection",
124
+ "RemoteControlConnectionClosed",
123
125
  "RemoteControlDesktop",
124
126
  "RemoteControlEnrollment",
125
127
  "RemoteControlEnvironment",
@@ -19,6 +19,10 @@ if TYPE_CHECKING:
19
19
  REMOTE_CONTROL_ROOT = "/wham/remote/control"
20
20
 
21
21
 
22
+ class RemoteControlConnectionClosed(ConnectionError):
23
+ """The Remote Control peer closed its WebSocket without an envelope."""
24
+
25
+
22
26
  class RemoteControlEnrollment(CodexBaseModel):
23
27
  server_id: str
24
28
  environment_id: str
@@ -117,8 +121,16 @@ class RemoteControlConnection:
117
121
  raise TypeError("Expected `envelope` to serialize to a JSON object.")
118
122
  self._socket.send(json.dumps(payload, separators=(",", ":")))
119
123
 
124
+ def ping(self, payload: bytes = b"") -> None:
125
+ """Send a WebSocket ping frame to keep the remote path alive."""
126
+ self._socket.ping(payload)
127
+
120
128
  def receive(self) -> dict[str, Any]:
121
129
  message = self._socket.recv()
130
+ if message in {"", b""}:
131
+ raise RemoteControlConnectionClosed(
132
+ "Remote Control WebSocket closed without a final envelope."
133
+ )
122
134
  if isinstance(message, bytes):
123
135
  message = message.decode("utf-8")
124
136
  payload = json.loads(message)
@@ -353,13 +353,13 @@ class Responses:
353
353
  raise RuntimeError("Remote compaction v2 failed")
354
354
 
355
355
  compaction_items = [item for item in output if item.get("type") == "compaction"]
356
- if len(output) != 1 or len(compaction_items) != 1:
356
+ if len(compaction_items) != 1:
357
357
  raise RuntimeError(
358
- "Remote compaction v2 expected exactly one compaction output item, "
358
+ "Remote compaction v2 expected one compaction output item, "
359
359
  f"received {len(compaction_items)} from {len(output)} output items"
360
360
  )
361
361
  return CompactedResponse(
362
362
  id=completed.get("id", ""),
363
- output=compaction_items,
363
+ output=output,
364
364
  usage=_usage_from_backend(completed.get("usage")),
365
365
  )
@@ -139,10 +139,13 @@ instance. Streaming is not exposed by `parse()`.
139
139
  | `text` | object or unset | Text verbosity/format controls. |
140
140
  | `tools`, `tool_choice` | JSON-compatible | Tool context retained during compaction. |
141
141
  | `parallel_tool_calls`, `prompt_cache_key`, `service_tier` | optional | Forwarded when supplied. |
142
- Returns `CompactedResponse`. Its `output` contains the single opaque encrypted
143
- `compaction` item returned by explicit remote compaction v2; `usage` preserves
144
- backend token accounting. The public helper remains stable even though the
145
- backend transport moved from `/responses/compact` to `/responses`.
142
+ Returns `CompactedResponse`. Its ordered `output` preserves every item emitted
143
+ by explicit remote compaction v2 and must contain exactly one opaque encrypted
144
+ `compaction` item; `usage` preserves backend token accounting. The current
145
+ backend normally emits only that anchor, but the SDK also retains any nonopaque
146
+ items a future or model-dependent response may carry. The public helper remains
147
+ stable even though the backend transport moved from `/responses/compact` to
148
+ `/responses`.
146
149
 
147
150
  #### `client.responses.websocket`
148
151
 
@@ -271,7 +271,7 @@ explicit remote-compaction-v2 contract on the normal streaming Responses route.
271
271
  }
272
272
  ```
273
273
 
274
- **Response** — SSE containing exactly one completed compaction item:
274
+ **Response** — SSE currently observed with exactly one completed compaction item:
275
275
  ```json
276
276
  {
277
277
  "id": "resp_...",
@@ -281,6 +281,8 @@ explicit remote-compaction-v2 contract on the normal streaming Responses route.
281
281
  }
282
282
  ```
283
283
  - `output` replaces the original history; pass it as `input` in subsequent calls.
284
+ - The SDK preserves any additional nonopaque output items in their emitted order
285
+ while still requiring exactly one opaque compaction anchor.
284
286
  - The `compaction` item is opaque on the client side.
285
287
 
286
288
  **SDK method**: `client.responses.compact(...)`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-backend-sdk"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Unofficial Python SDK for the ChatGPT Codex backend API"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -3,13 +3,18 @@ import json
3
3
  import pytest
4
4
 
5
5
  from codex_backend_sdk import OpenAI
6
- from codex_backend_sdk.resources.remote_control import RemoteControlEnrollment
6
+ from codex_backend_sdk.resources.remote_control import (
7
+ RemoteControlConnection,
8
+ RemoteControlConnectionClosed,
9
+ RemoteControlEnrollment,
10
+ )
7
11
 
8
12
 
9
13
  class FakeSocket:
10
14
  def __init__(self):
11
15
  self.connected = True
12
16
  self.sent = []
17
+ self.pings = []
13
18
  self.received = [json.dumps({"type": "ping", "cursor": "cursor-2"})]
14
19
 
15
20
  def send(self, message):
@@ -18,6 +23,9 @@ class FakeSocket:
18
23
  def recv(self):
19
24
  return self.received.pop(0)
20
25
 
26
+ def ping(self, payload=b""):
27
+ self.pings.append(payload)
28
+
21
29
  def close(self):
22
30
  self.connected = False
23
31
 
@@ -277,6 +285,7 @@ def test_remote_control_connection_builds_codex_headers_and_tracks_cursor():
277
285
  websocket_factory=factory,
278
286
  )
279
287
  connection.send({"type": "ack", "client_id": "client-1"})
288
+ connection.ping(b"heartbeat")
280
289
  received = connection.receive()
281
290
  connection.close()
282
291
 
@@ -287,6 +296,25 @@ def test_remote_control_connection_builds_codex_headers_and_tracks_cursor():
287
296
  assert "Authorization: Bearer remote-token" in headers
288
297
  assert "x-codex-subscribe-cursor: cursor-1" in headers
289
298
  assert json.loads(socket.sent[0]) == {"type": "ack", "client_id": "client-1"}
299
+ assert socket.pings == [b"heartbeat"]
290
300
  assert received["type"] == "ping"
291
301
  assert connection.subscribe_cursor == "cursor-2"
292
302
  assert connection.connected is False
303
+
304
+
305
+ @pytest.mark.parametrize("closed_frame", ["", b""])
306
+ def test_remote_control_connection_reports_an_empty_close_frame(closed_frame):
307
+ socket = FakeSocket()
308
+ socket.received = [closed_frame]
309
+ connection = RemoteControlConnection(
310
+ socket,
311
+ websocket_url="wss://example.test/remote",
312
+ enrollment=enrollment(),
313
+ installation_id="installation-1",
314
+ server_name="Desktop",
315
+ protocol_version="3",
316
+ subscribe_cursor="stale-cursor",
317
+ )
318
+
319
+ with pytest.raises(RemoteControlConnectionClosed, match="closed without"):
320
+ connection.receive()
@@ -400,6 +400,43 @@ def test_responses_compact_sends_shared_request_fields():
400
400
  assert compacted.output == [{"type": "compaction", "encrypted_content": "opaque"}]
401
401
 
402
402
 
403
+ def test_responses_compact_preserves_nonopaque_output_items_in_backend_order():
404
+ class PreservingFakeClient(FakeClient):
405
+ def _post(self, path, *, body, stream=False, **options):
406
+ self.posts.append((path, body, stream, options))
407
+ return FakeSSE([
408
+ (
409
+ 'data: {"type":"response.output_item.done","item":'
410
+ '{"type":"message","role":"assistant","content":'
411
+ '[{"type":"output_text","text":"Recent work"}]}}'
412
+ ),
413
+ "",
414
+ (
415
+ 'data: {"type":"response.output_item.done","item":'
416
+ '{"type":"compaction","encrypted_content":"opaque"}}'
417
+ ),
418
+ "",
419
+ (
420
+ 'data: {"type":"response.completed","response":'
421
+ '{"id":"resp_preserved","model":"gpt-test",'
422
+ '"usage":{"input_tokens":11,"output_tokens":7,"total_tokens":18}}}'
423
+ ),
424
+ ])
425
+
426
+ compacted = PreservingFakeClient().responses.compact(
427
+ input=[{"role": "user", "content": "Long context"}],
428
+ )
429
+
430
+ assert compacted.output == [
431
+ {
432
+ "type": "message",
433
+ "role": "assistant",
434
+ "content": [{"type": "output_text", "text": "Recent work"}],
435
+ },
436
+ {"type": "compaction", "encrypted_content": "opaque"},
437
+ ]
438
+
439
+
403
440
  def test_responses_create_rejects_official_params_not_exposed_by_codex_backend():
404
441
  client = FakeClient()
405
442
 
@@ -160,7 +160,7 @@ wheels = [
160
160
 
161
161
  [[package]]
162
162
  name = "codex-backend-sdk"
163
- version = "0.5.0"
163
+ version = "0.5.2"
164
164
  source = { editable = "." }
165
165
  dependencies = [
166
166
  { name = "pydantic" },