codex-backend-sdk 0.5.2__tar.gz → 0.5.3__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 (75) hide show
  1. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/CHANGELOG.md +19 -1
  2. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/PKG-INFO +39 -24
  3. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/README.md +34 -19
  4. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/__init__.py +9 -1
  5. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/_client.py +3 -2
  6. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/_models.py +26 -0
  7. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/codex_client.py +8 -0
  8. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/chatgpt_plugins.py +101 -19
  9. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/codex.py +213 -23
  10. codex_backend_sdk-0.5.3/codex_backend_sdk/resources/realtime.py +437 -0
  11. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/docs/api-reference.md +25 -1
  12. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/docs/backend-api.md +27 -13
  13. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/docs/desktop-endpoint-inventory.md +53 -7
  14. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/docs/endpoint-coverage.md +19 -8
  15. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/docs/openai-compatibility.md +9 -2
  16. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/pyproject.toml +5 -5
  17. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/conftest.py +2 -1
  18. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_basic.py +5 -2
  19. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_plugins.py +59 -13
  20. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_codex_resources.py +148 -22
  21. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_openai_compatibility.py +36 -18
  22. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_realtime_resource.py +72 -0
  23. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/uv.lock +80 -28
  24. codex_backend_sdk-0.5.2/codex_backend_sdk/resources/realtime.py +0 -171
  25. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/.gitignore +0 -0
  26. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/LICENSE +0 -0
  27. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/_streaming.py +0 -0
  28. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/_transport.py +0 -0
  29. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/_utils.py +0 -0
  30. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/oauth.py +0 -0
  31. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/pkce.py +0 -0
  32. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/__init__.py +0 -0
  33. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
  34. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/chatgpt.py +0 -0
  35. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/chatgpt_apps.py +0 -0
  36. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/chatgpt_connectors.py +0 -0
  37. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/chatgpt_plugin_bundles.py +0 -0
  38. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/chatgpt_plugin_shares.py +0 -0
  39. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/chatgpt_writing_blocks.py +0 -0
  40. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/codex_web_search.py +0 -0
  41. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/files.py +0 -0
  42. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/images.py +0 -0
  43. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/models.py +0 -0
  44. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/openai_oauth.py +0 -0
  45. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/remote_control.py +0 -0
  46. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/responses.py +0 -0
  47. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/responses_websocket.py +0 -0
  48. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/resources/worktree_snapshots.py +0 -0
  49. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/codex_backend_sdk/storage.py +0 -0
  50. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/examples/agent.py +0 -0
  51. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_account_info.py +0 -0
  52. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_apps.py +0 -0
  53. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_connectors.py +0 -0
  54. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_plugin_bundles.py +0 -0
  55. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_plugin_shares.py +0 -0
  56. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_projects_gizmos.py +0 -0
  57. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_resources.py +0 -0
  58. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_search_files.py +0 -0
  59. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_chatgpt_writing_blocks.py +0 -0
  60. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_client_retry.py +0 -0
  61. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_codex_web_search.py +0 -0
  62. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_conversation.py +0 -0
  63. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_files_resource.py +0 -0
  64. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_hosted_mcp.py +0 -0
  65. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_images.py +0 -0
  66. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_oauth.py +0 -0
  67. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_openai_oauth_resources.py +0 -0
  68. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_prompt_cache_usage.py +0 -0
  69. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_reasoning.py +0 -0
  70. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_remote_control_resource.py +0 -0
  71. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_responses_resource.py +0 -0
  72. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_responses_websocket.py +0 -0
  73. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_structured_output.py +0 -0
  74. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_tools.py +0 -0
  75. {codex_backend_sdk-0.5.2 → codex_backend_sdk-0.5.3}/tests/test_worktree_snapshots.py +0 -0
@@ -2,7 +2,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## Unreleased
5
+ ## [0.5.3] - 2026-09-18
6
+
7
+ ### Added
8
+ - Added the OpenAI-shaped `client.live.create(...)` and
9
+ `client.live.sideband.connect(...)` surfaces over the existing Codex OAuth
10
+ Realtime v3 transport, including the public response shape and lazy
11
+ sideband context-manager idiom.
12
+ - Added current Codex workspace usage, task-usage, turn-estimate, plugin/skill
13
+ analytics, and reset-credit history resources discovered by the refreshed
14
+ source scan.
15
+ - Added the current Desktop plugin home feed and explicit plugin/skill
16
+ enable-disable mutations.
17
+
18
+ ### Changed
19
+ - Updated the compatibility baseline to `openai-python` 3.16.1 while retaining
20
+ the additive low-level `client.realtime` API.
21
+ - Reconciled the endpoint inventory with the refreshed Codex and official
22
+ Desktop checkouts, and tightened validation of plugin catalog responses and
23
+ pagination state.
6
24
 
7
25
  ## [0.5.1] - 2026-08-14
8
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codex-backend-sdk
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -19,12 +19,12 @@ Requires-Dist: pydantic>=2.0
19
19
  Requires-Dist: requests>=2.28
20
20
  Requires-Dist: websocket-client>=1.8
21
21
  Provides-Extra: dev
22
- Requires-Dist: openai<2.53.0,>=1.0; (python_version < '3.10') and extra == 'dev'
23
- Requires-Dist: openai>=2.53.0; (python_version >= '3.10') and extra == 'dev'
22
+ Requires-Dist: openai<3.0,>=1.0; (python_version < '3.10') and extra == 'dev'
23
+ Requires-Dist: openai>=3.16.1; (python_version >= '3.10') and extra == 'dev'
24
24
  Requires-Dist: pytest>=7.0; extra == 'dev'
25
25
  Provides-Extra: openai
26
- Requires-Dist: openai<2.53.0,>=1.0; (python_version < '3.10') and extra == 'openai'
27
- Requires-Dist: openai>=2.53.0; (python_version >= '3.10') and extra == 'openai'
26
+ Requires-Dist: openai<3.0,>=1.0; (python_version < '3.10') and extra == 'openai'
27
+ Requires-Dist: openai>=3.16.1; (python_version >= '3.10') and extra == 'openai'
28
28
  Description-Content-Type: text/markdown
29
29
 
30
30
  # codex-backend-sdk
@@ -65,7 +65,7 @@ from codex_backend_sdk import OpenAI
65
65
  client = OpenAI().authenticate()
66
66
 
67
67
  response = client.responses.create(
68
- model="gpt-5.4",
68
+ model="gpt-5.5",
69
69
  input="Explain quicksort in one paragraph.",
70
70
  )
71
71
 
@@ -89,7 +89,8 @@ client.files # upload files for Apps/MCP parameters
89
89
  client.images # Codex-backed image generation and editing
90
90
  client.audio # ChatGPT-backed transcription
91
91
  client.embeddings # OpenAI embeddings endpoint through OAuth
92
- client.realtime # WebRTC call creation and Realtime connection headers
92
+ client.live # OpenAI-shaped GPT-Live creation and sideband attachment
93
+ client.realtime # additive low-level Codex Realtime v3 transport
93
94
  ```
94
95
 
95
96
  These resources follow `openai-python` conventions where the backend overlaps
@@ -142,7 +143,7 @@ dictionaries or raw streams when imposing a stable model would be misleading.
142
143
 
143
144
  ```python
144
145
  response = client.responses.create(
145
- model="gpt-5.4",
146
+ model="gpt-5.5",
146
147
  instructions="Be concise.",
147
148
  input="Summarize the CAP theorem.",
148
149
  reasoning={"effort": "medium", "summary": "auto"},
@@ -173,7 +174,7 @@ connection reusable across sequential turns:
173
174
 
174
175
  ```python
175
176
  with client.responses.websocket.connect() as ws:
176
- for event in ws.create({"model": "gpt-5.4", "input": "Say hello."}):
177
+ for event in ws.create({"model": "gpt-5.5", "input": "Say hello."}):
177
178
  if event.get("type") == "response.output_text.delta":
178
179
  print(event.get("delta", ""), end="")
179
180
  ```
@@ -197,7 +198,7 @@ print(client.responses.create(input=history).output_text)
197
198
 
198
199
  ```python
199
200
  compacted = client.responses.compact(
200
- model="gpt-5.4",
201
+ model="gpt-5.5",
201
202
  instructions="Preserve task-critical decisions and unresolved work.",
202
203
  input=history,
203
204
  )
@@ -268,7 +269,7 @@ print(result.output_parsed)
268
269
  for model in client.models.list():
269
270
  print(model.id, model.context_window, model.supported_reasoning_levels)
270
271
 
271
- model = client.models.retrieve("gpt-5.4")
272
+ model = client.models.retrieve("gpt-5.5")
272
273
  ```
273
274
 
274
275
  ### Upload a file
@@ -340,6 +341,10 @@ sports, and time commands. See the API reference for command validation.
340
341
  quota = client.codex.usage()
341
342
  daily = client.codex.usage_details.daily_token_breakdown()
342
343
  credits = client.codex.usage_details.credit_events()
344
+ plugin_metrics = client.codex.usage_details.plugin_usage_metrics(
345
+ start_date="2026-09-01",
346
+ end_date="2026-09-18",
347
+ )
343
348
 
344
349
  print(quota.get("rate_limit", {}).get("primary_window"))
345
350
  ```
@@ -443,6 +448,7 @@ tool annotations and own user confirmation; the SDK never auto-invokes tools.
443
448
  plugins = client.chatgpt.plugins.list_all(scope="GLOBAL")
444
449
  installed = client.chatgpt.plugins.installed_all()
445
450
  suggested = client.chatgpt.plugins.suggested()
451
+ home = client.chatgpt.plugins.home()
446
452
  skill = client.chatgpt.plugins.skill("plugins~...", "skill-name")
447
453
 
448
454
  checkout = client.chatgpt.plugins.bundles.extract_plugin(
@@ -491,33 +497,42 @@ return a typed in-memory object, bytes, `BytesIO`, a data URI, or a file path.
491
497
  ### Realtime voice
492
498
 
493
499
  ```python
494
- answer = client.realtime.calls.create_v3(
495
- sdp=offer_sdp,
500
+ live = client.live.create(
496
501
  session={
497
502
  "model": "gpt-live-1-codex",
498
503
  "instructions": "Speak naturally and stay concise.",
504
+ "delegation": {"type": "client"},
499
505
  },
506
+ transport={"type": "webrtc", "sdp": offer_sdp},
500
507
  )
501
- print(answer.answer_sdp)
508
+ apply_remote_sdp(live.transport.sdp)
509
+
510
+ with client.live.sideband.connect(session_id=live.session.id) as sideband:
511
+ for event in sideband:
512
+ print(event.type)
502
513
  ```
503
514
 
504
515
  Live probes confirmed `gpt-live-1-codex` and
505
516
  `gpt-live-1-boulder-alpha`. They are Codex Realtime v3 snapshots, not aliases
506
- for the public `gpt-realtime` family. The call response exposes `call_id`; join
507
- its OAuth-authenticated control and delegation channel with:
517
+ for the public `gpt-live-1` model. `client.live` follows the current
518
+ `openai-python` creation, response, and sideband idioms while adapting them to
519
+ the ChatGPT OAuth call transport.
520
+
521
+ The earlier low-level surface remains available for backend-specific code:
508
522
 
509
523
  ```python
510
- sideband = client.realtime.sideband.connect(
511
- call_id=answer.call_id,
512
- session_id="voice-session",
524
+ answer = client.realtime.calls.create_v3(
525
+ sdp=offer_sdp,
526
+ session={"model": "gpt-live-1-codex"},
513
527
  )
514
- sideband.send({"type": "delegation.context.append", "content": []})
515
- event = sideband.recv()
516
- sideband.close()
528
+ with client.realtime.sideband.connect(call_id=answer.call_id) as sideband:
529
+ sideband.send({"type": "session.close"})
517
530
  ```
518
531
 
519
- The SDK intentionally exposes only the ChatGPT-authenticated v3 transport. It
520
- does not retain the separately billed API-key Voice v2 WebSocket helpers.
532
+ The OAuth backend's event dialect is still experimental and is exposed
533
+ forward-compatibly through `LiveEvent` rather than rewritten into invented
534
+ public fields. The public primary `client.live.connect()` WebSocket is not
535
+ available through ChatGPT OAuth; use WebRTC plus the attached sideband.
521
536
 
522
537
  ### Embeddings
523
538
 
@@ -36,7 +36,7 @@ from codex_backend_sdk import OpenAI
36
36
  client = OpenAI().authenticate()
37
37
 
38
38
  response = client.responses.create(
39
- model="gpt-5.4",
39
+ model="gpt-5.5",
40
40
  input="Explain quicksort in one paragraph.",
41
41
  )
42
42
 
@@ -60,7 +60,8 @@ client.files # upload files for Apps/MCP parameters
60
60
  client.images # Codex-backed image generation and editing
61
61
  client.audio # ChatGPT-backed transcription
62
62
  client.embeddings # OpenAI embeddings endpoint through OAuth
63
- client.realtime # WebRTC call creation and Realtime connection headers
63
+ client.live # OpenAI-shaped GPT-Live creation and sideband attachment
64
+ client.realtime # additive low-level Codex Realtime v3 transport
64
65
  ```
65
66
 
66
67
  These resources follow `openai-python` conventions where the backend overlaps
@@ -113,7 +114,7 @@ dictionaries or raw streams when imposing a stable model would be misleading.
113
114
 
114
115
  ```python
115
116
  response = client.responses.create(
116
- model="gpt-5.4",
117
+ model="gpt-5.5",
117
118
  instructions="Be concise.",
118
119
  input="Summarize the CAP theorem.",
119
120
  reasoning={"effort": "medium", "summary": "auto"},
@@ -144,7 +145,7 @@ connection reusable across sequential turns:
144
145
 
145
146
  ```python
146
147
  with client.responses.websocket.connect() as ws:
147
- for event in ws.create({"model": "gpt-5.4", "input": "Say hello."}):
148
+ for event in ws.create({"model": "gpt-5.5", "input": "Say hello."}):
148
149
  if event.get("type") == "response.output_text.delta":
149
150
  print(event.get("delta", ""), end="")
150
151
  ```
@@ -168,7 +169,7 @@ print(client.responses.create(input=history).output_text)
168
169
 
169
170
  ```python
170
171
  compacted = client.responses.compact(
171
- model="gpt-5.4",
172
+ model="gpt-5.5",
172
173
  instructions="Preserve task-critical decisions and unresolved work.",
173
174
  input=history,
174
175
  )
@@ -239,7 +240,7 @@ print(result.output_parsed)
239
240
  for model in client.models.list():
240
241
  print(model.id, model.context_window, model.supported_reasoning_levels)
241
242
 
242
- model = client.models.retrieve("gpt-5.4")
243
+ model = client.models.retrieve("gpt-5.5")
243
244
  ```
244
245
 
245
246
  ### Upload a file
@@ -311,6 +312,10 @@ sports, and time commands. See the API reference for command validation.
311
312
  quota = client.codex.usage()
312
313
  daily = client.codex.usage_details.daily_token_breakdown()
313
314
  credits = client.codex.usage_details.credit_events()
315
+ plugin_metrics = client.codex.usage_details.plugin_usage_metrics(
316
+ start_date="2026-09-01",
317
+ end_date="2026-09-18",
318
+ )
314
319
 
315
320
  print(quota.get("rate_limit", {}).get("primary_window"))
316
321
  ```
@@ -414,6 +419,7 @@ tool annotations and own user confirmation; the SDK never auto-invokes tools.
414
419
  plugins = client.chatgpt.plugins.list_all(scope="GLOBAL")
415
420
  installed = client.chatgpt.plugins.installed_all()
416
421
  suggested = client.chatgpt.plugins.suggested()
422
+ home = client.chatgpt.plugins.home()
417
423
  skill = client.chatgpt.plugins.skill("plugins~...", "skill-name")
418
424
 
419
425
  checkout = client.chatgpt.plugins.bundles.extract_plugin(
@@ -462,33 +468,42 @@ return a typed in-memory object, bytes, `BytesIO`, a data URI, or a file path.
462
468
  ### Realtime voice
463
469
 
464
470
  ```python
465
- answer = client.realtime.calls.create_v3(
466
- sdp=offer_sdp,
471
+ live = client.live.create(
467
472
  session={
468
473
  "model": "gpt-live-1-codex",
469
474
  "instructions": "Speak naturally and stay concise.",
475
+ "delegation": {"type": "client"},
470
476
  },
477
+ transport={"type": "webrtc", "sdp": offer_sdp},
471
478
  )
472
- print(answer.answer_sdp)
479
+ apply_remote_sdp(live.transport.sdp)
480
+
481
+ with client.live.sideband.connect(session_id=live.session.id) as sideband:
482
+ for event in sideband:
483
+ print(event.type)
473
484
  ```
474
485
 
475
486
  Live probes confirmed `gpt-live-1-codex` and
476
487
  `gpt-live-1-boulder-alpha`. They are Codex Realtime v3 snapshots, not aliases
477
- for the public `gpt-realtime` family. The call response exposes `call_id`; join
478
- its OAuth-authenticated control and delegation channel with:
488
+ for the public `gpt-live-1` model. `client.live` follows the current
489
+ `openai-python` creation, response, and sideband idioms while adapting them to
490
+ the ChatGPT OAuth call transport.
491
+
492
+ The earlier low-level surface remains available for backend-specific code:
479
493
 
480
494
  ```python
481
- sideband = client.realtime.sideband.connect(
482
- call_id=answer.call_id,
483
- session_id="voice-session",
495
+ answer = client.realtime.calls.create_v3(
496
+ sdp=offer_sdp,
497
+ session={"model": "gpt-live-1-codex"},
484
498
  )
485
- sideband.send({"type": "delegation.context.append", "content": []})
486
- event = sideband.recv()
487
- sideband.close()
499
+ with client.realtime.sideband.connect(call_id=answer.call_id) as sideband:
500
+ sideband.send({"type": "session.close"})
488
501
  ```
489
502
 
490
- The SDK intentionally exposes only the ChatGPT-authenticated v3 transport. It
491
- does not retain the separately billed API-key Voice v2 WebSocket helpers.
503
+ The OAuth backend's event dialect is still experimental and is exposed
504
+ forward-compatibly through `LiveEvent` rather than rewritten into invented
505
+ public fields. The public primary `client.live.connect()` WebSocket is not
506
+ available through ChatGPT OAuth; use WebRTC plus the attached sideband.
492
507
 
493
508
  ### Embeddings
494
509
 
@@ -17,7 +17,7 @@ Quickstart:
17
17
  print(response.output_text)
18
18
  """
19
19
 
20
- __version__ = "0.5.2"
20
+ __version__ = "0.5.3"
21
21
 
22
22
  from .oauth import (
23
23
  DeviceCode,
@@ -57,6 +57,10 @@ from .codex_client import (
57
57
  Image,
58
58
  ImageResponse,
59
59
  ImagesResponse,
60
+ LiveCreateResponse,
61
+ LiveEvent,
62
+ LiveSession,
63
+ LiveTransport,
60
64
  RateLimitResetCredit,
61
65
  RateLimitResetCredits,
62
66
  ConsumeRateLimitResetCreditResponse,
@@ -102,6 +106,10 @@ __all__ = [
102
106
  "Image",
103
107
  "ImageResponse",
104
108
  "ImagesResponse",
109
+ "LiveCreateResponse",
110
+ "LiveEvent",
111
+ "LiveSession",
112
+ "LiveTransport",
105
113
  "RateLimitResetCredit",
106
114
  "RateLimitResetCredits",
107
115
  "ConsumeRateLimitResetCreditResponse",
@@ -30,7 +30,7 @@ class CodexClient:
30
30
  self,
31
31
  *,
32
32
  store: Optional[TokenStore] = None,
33
- model: str = "gpt-5.4",
33
+ model: str = "gpt-5.5",
34
34
  instructions: Optional[str] = None,
35
35
  timeout: float = 120,
36
36
  max_retries: int = 2,
@@ -44,7 +44,7 @@ class CodexClient:
44
44
  from .resources.openai_oauth import Audio, Embeddings
45
45
  from .resources.files import Files
46
46
  from .resources.images import Images
47
- from .resources.realtime import Realtime
47
+ from .resources.realtime import Live, Realtime
48
48
  from .resources.responses import Responses
49
49
 
50
50
  self._store = store
@@ -66,6 +66,7 @@ class CodexClient:
66
66
  self.responses = Responses(self)
67
67
  self.models = Models(self)
68
68
  self.realtime = Realtime(self)
69
+ self.live = Live(self)
69
70
  self.embeddings = Embeddings(self)
70
71
  self.audio = Audio(self)
71
72
  self.images = Images(self)
@@ -435,6 +435,32 @@ class RealtimeCallResponse(BinaryResponseContent):
435
435
  )
436
436
 
437
437
 
438
+ class LiveSession(CodexBaseModel):
439
+ """Resolved Live session identity returned by the OAuth adapter."""
440
+
441
+ id: str
442
+
443
+
444
+ class LiveTransport(CodexBaseModel):
445
+ """WebRTC answer using the public OpenAI Live response shape."""
446
+
447
+ type: Literal["webrtc"] = "webrtc"
448
+ sdp: str
449
+
450
+
451
+ class LiveCreateResponse(CodexBaseModel):
452
+ """OpenAI-compatible view of a Codex OAuth Realtime call response."""
453
+
454
+ session: LiveSession
455
+ transport: LiveTransport
456
+
457
+
458
+ class LiveEvent(CodexBaseModel):
459
+ """Forward-compatible Live sideband event with attribute access."""
460
+
461
+ type: str
462
+
463
+
438
464
  def _is_uuid(value: str) -> bool:
439
465
  if len(value) != 36:
440
466
  return False
@@ -18,6 +18,10 @@ from ._models import (
18
18
  ImageData,
19
19
  ImageResponse,
20
20
  ImagesResponse,
21
+ LiveCreateResponse,
22
+ LiveEvent,
23
+ LiveSession,
24
+ LiveTransport,
21
25
  Model,
22
26
  MemorySummarizeOutput,
23
27
  MemorySummarizeResponse,
@@ -55,6 +59,10 @@ __all__ = [
55
59
  "ImageData",
56
60
  "ImageResponse",
57
61
  "ImagesResponse",
62
+ "LiveCreateResponse",
63
+ "LiveEvent",
64
+ "LiveSession",
65
+ "LiveTransport",
58
66
  "CompactedResponse",
59
67
  "MemorySummarizeOutput",
60
68
  "MemorySummarizeResponse",
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Literal, TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any, Literal
6
6
  from urllib.parse import quote
7
7
 
8
8
  if TYPE_CHECKING:
@@ -23,13 +23,18 @@ class ChatGPTPlugins:
23
23
  payload = self._client._get_chatgpt(
24
24
  "/plugins/featured", params={"platform": platform}
25
25
  )
26
- if not isinstance(payload, list) or not all(isinstance(item, str) for item in payload):
26
+ if not isinstance(payload, list) or not all(
27
+ isinstance(item, str) for item in payload
28
+ ):
27
29
  raise RuntimeError("Featured plugins returned an invalid plugin-id list.")
28
30
  return payload
29
31
 
30
32
  def curated_export(self) -> dict[str, Any]:
31
33
  payload = self._client._get_chatgpt("/plugins/export/curated")
32
- if not isinstance(payload.get("download_url"), str) or not payload["download_url"]:
34
+ if (
35
+ not isinstance(payload.get("download_url"), str)
36
+ or not payload["download_url"]
37
+ ):
33
38
  raise RuntimeError("Curated plugin export is missing its download URL.")
34
39
  return payload
35
40
 
@@ -46,8 +51,16 @@ class ChatGPTPlugins:
46
51
  params={
47
52
  "scope": _scope(scope),
48
53
  "limit": _positive(limit, "limit"),
49
- **({} if collection is None else {"collection": _required(collection, "collection")}),
50
- **({} if page_token is None else {"pageToken": _required(page_token, "page_token")}),
54
+ **(
55
+ {}
56
+ if collection is None
57
+ else {"collection": _required(collection, "collection")}
58
+ ),
59
+ **(
60
+ {}
61
+ if page_token is None
62
+ else {"pageToken": _required(page_token, "page_token")}
63
+ ),
51
64
  },
52
65
  )
53
66
 
@@ -77,7 +90,11 @@ class ChatGPTPlugins:
77
90
  "q": _required(query, "query"),
78
91
  "limit": _positive(limit, "limit"),
79
92
  **({} if scope is None else {"scope": _scope(scope)}),
80
- **({} if page_token is None else {"pageToken": _required(page_token, "page_token")}),
93
+ **(
94
+ {}
95
+ if page_token is None
96
+ else {"pageToken": _required(page_token, "page_token")}
97
+ ),
81
98
  },
82
99
  )
83
100
 
@@ -124,22 +141,35 @@ class ChatGPTPlugins:
124
141
  "/ps/plugins/workspace/shared",
125
142
  params={
126
143
  "limit": _positive(limit, "limit"),
127
- **({} if page_token is None else {"pageToken": _required(page_token, "page_token")}),
144
+ **(
145
+ {}
146
+ if page_token is None
147
+ else {"pageToken": _required(page_token, "page_token")}
148
+ ),
128
149
  },
129
150
  )
130
151
 
152
+ def home(self) -> dict[str, Any]:
153
+ """Return the curated plugin home sections used by current Desktop."""
154
+ payload = self._client._get_chatgpt("/ps/plugins/home", headers=_headers())
155
+ if not isinstance(payload.get("sections"), list):
156
+ raise TypeError("Plugin home response contains invalid sections.")
157
+ return payload
158
+
131
159
  def suggested(
132
160
  self,
133
161
  *,
134
162
  scope: Literal["GLOBAL", "USER", "WORKSPACE"] = "GLOBAL",
135
163
  ) -> dict[str, Any]:
136
164
  payload = self._client._get_chatgpt(
137
- "/ps/plugins/suggested",
165
+ "/ps/plugins/suggested/codex",
138
166
  params={"scope": _scope(scope)},
139
167
  headers=_headers(),
140
168
  )
141
169
  if not isinstance(payload.get("plugins"), list):
142
- raise RuntimeError("Suggested plugins response contains an invalid plugin list.")
170
+ raise RuntimeError(
171
+ "Suggested plugins response contains an invalid plugin list."
172
+ )
143
173
  return payload
144
174
 
145
175
  def retrieve(
@@ -150,9 +180,7 @@ class ChatGPTPlugins:
150
180
  ) -> dict[str, Any]:
151
181
  payload = self._client._get_chatgpt(
152
182
  f"/ps/plugins/{_path(plugin_id, 'plugin_id')}",
153
- params=(
154
- {"includeDownloadUrls": True} if include_download_urls else None
155
- ),
183
+ params=({"includeDownloadUrls": True} if include_download_urls else None),
156
184
  headers=_headers(),
157
185
  )
158
186
  if not isinstance(payload.get("id"), str):
@@ -170,12 +198,18 @@ class ChatGPTPlugins:
170
198
  headers=_headers(),
171
199
  )
172
200
  if payload.get("plugin_id") != expected_id:
173
- raise RuntimeError("Plugin skill response returned an unexpected plugin ID.")
201
+ raise RuntimeError(
202
+ "Plugin skill response returned an unexpected plugin ID."
203
+ )
174
204
  if payload.get("name") != expected_name:
175
- raise RuntimeError("Plugin skill response returned an unexpected skill name.")
205
+ raise RuntimeError(
206
+ "Plugin skill response returned an unexpected skill name."
207
+ )
176
208
  contents = payload.get("skill_md_contents")
177
209
  if contents is not None and not isinstance(contents, str):
178
- raise RuntimeError("Plugin skill response returned invalid Markdown contents.")
210
+ raise RuntimeError(
211
+ "Plugin skill response returned invalid Markdown contents."
212
+ )
179
213
  return payload
180
214
 
181
215
  def _page(self, path: str, *, params: dict[str, Any]) -> dict[str, Any]:
@@ -195,9 +229,13 @@ class ChatGPTPlugins:
195
229
  if token_value is None:
196
230
  return plugins
197
231
  if not isinstance(token_value, str) or not token_value:
198
- raise RuntimeError("Plugin catalog returned an invalid next page token.")
232
+ raise RuntimeError(
233
+ "Plugin catalog returned an invalid next page token."
234
+ )
199
235
  if token_value in seen:
200
- raise RuntimeError("Plugin catalog returned a repeated next page token.")
236
+ raise RuntimeError(
237
+ "Plugin catalog returned a repeated next page token."
238
+ )
201
239
  seen.add(token_value)
202
240
  token = token_value
203
241
 
@@ -224,6 +262,46 @@ class ChatGPTPluginInstallation:
224
262
  def uninstall(self, plugin_id: str) -> dict[str, Any]:
225
263
  return self._mutate(plugin_id, "uninstall", expected_enabled=False)
226
264
 
265
+ def enable(self, plugin_id: str) -> dict[str, Any]:
266
+ return self._set_enabled(plugin_id, "enable")
267
+
268
+ def disable(self, plugin_id: str) -> dict[str, Any]:
269
+ return self._set_enabled(plugin_id, "disable")
270
+
271
+ def enable_skill(self, plugin_id: str, skill_name: str) -> dict[str, Any]:
272
+ return self._set_skill_enabled(plugin_id, skill_name, "enable")
273
+
274
+ def disable_skill(self, plugin_id: str, skill_name: str) -> dict[str, Any]:
275
+ return self._set_skill_enabled(plugin_id, skill_name, "disable")
276
+
277
+ def _set_enabled(self, plugin_id: str, action: str) -> dict[str, Any]:
278
+ expected_id = _required(plugin_id, "plugin_id")
279
+ payload = self._client._request_chatgpt(
280
+ "POST",
281
+ f"/ps/plugins/{_path(expected_id, 'plugin_id')}/{action}",
282
+ headers=_headers(),
283
+ ).json()
284
+ if not isinstance(payload, dict):
285
+ raise TypeError("Plugin state mutation returned an invalid response.")
286
+ return payload
287
+
288
+ def _set_skill_enabled(
289
+ self, plugin_id: str, skill_name: str, action: str
290
+ ) -> dict[str, Any]:
291
+ expected_id = _required(plugin_id, "plugin_id")
292
+ expected_name = _required(skill_name, "skill_name")
293
+ payload = self._client._request_chatgpt(
294
+ "POST",
295
+ (
296
+ f"/ps/plugins/{_path(expected_id, 'plugin_id')}/skills/"
297
+ f"{_path(expected_name, 'skill_name')}/{action}"
298
+ ),
299
+ headers=_headers(),
300
+ ).json()
301
+ if not isinstance(payload, dict):
302
+ raise TypeError("Plugin skill mutation returned an invalid response.")
303
+ return payload
304
+
227
305
  def _mutate(
228
306
  self,
229
307
  plugin_id: str,
@@ -252,7 +330,9 @@ def _headers() -> dict[str, str]:
252
330
 
253
331
  def _required(value: str, name: str) -> str:
254
332
  if not value:
255
- raise ValueError(f"Expected a non-empty value for `{name}` but received {value!r}")
333
+ raise ValueError(
334
+ f"Expected a non-empty value for `{name}` but received {value!r}"
335
+ )
256
336
  return value
257
337
 
258
338
 
@@ -275,7 +355,9 @@ def _path(value: str, name: str) -> str:
275
355
  def _validate_page(payload: dict[str, Any]) -> None:
276
356
  plugins = payload.get("plugins")
277
357
  pagination = payload.get("pagination")
278
- if not isinstance(plugins, list) or not all(isinstance(item, dict) for item in plugins):
358
+ if not isinstance(plugins, list) or not all(
359
+ isinstance(item, dict) for item in plugins
360
+ ):
279
361
  raise RuntimeError("Plugin catalog response contains an invalid plugin list.")
280
362
  if not isinstance(pagination, dict):
281
363
  raise RuntimeError("Plugin catalog response contains invalid pagination.")