codex-backend-sdk 0.1.1__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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.whl
8
+ .env
9
+ .venv/
10
+ venv/
11
+ .pytest_cache/
12
+ .claude/
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-05-03
4
+
5
+ - Add `CodexClient.abort()` for stopping the active SSE response stream.
6
+ - Export `ResponseAborted` and raise it when an aborted stream unwinds.
7
+ - Document stream abort usage and add regression coverage for abort behavior.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 codex-sdk contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,369 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-backend-sdk
3
+ Version: 0.1.1
4
+ Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: chatgpt,codex,llm,openai,sdk
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Requires-Dist: requests>=2.28
19
+ Provides-Extra: dev
20
+ Requires-Dist: openai>=1.0; extra == 'dev'
21
+ Requires-Dist: pytest>=7.0; extra == 'dev'
22
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
23
+ Provides-Extra: examples
24
+ Requires-Dist: pyyaml>=6.0; extra == 'examples'
25
+ Provides-Extra: openai
26
+ Requires-Dist: openai>=1.0; extra == 'openai'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # codex-backend-sdk
30
+
31
+ Unofficial Python SDK for the ChatGPT Codex backend API (`chatgpt.com/backend-api/codex`).
32
+
33
+ This is a **lower-level** alternative to the official Codex CLI/SDK. It gives direct access to the underlying HTTP API endpoints on which the CLI relies, so you can build your own agent loop from scratch without inheriting OpenAI's design choices.
34
+
35
+ > **Requirements:** a ChatGPT Plus, Pro, or Enterprise subscription. No OpenAI API key and no Codex CLI installation needed — authentication goes through ChatGPT OAuth directly from Python.
36
+
37
+ > [!WARNING]
38
+ > **Disclaimer:** This is an independent, community-maintained library that reverse-engineers undocumented endpoints of `chatgpt.com`. It is **not** affiliated with, endorsed by, or supported by OpenAI. Usage remains subject to [OpenAI's Terms of Use](https://openai.com/policies/terms-of-use). Endpoints may change or break without notice.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ git clone https://github.com/B4PT0R/codex-backend-sdk.git
46
+ cd codex-backend-sdk
47
+ pip install -e .
48
+ ```
49
+
50
+ For the agent example (`examples/agent.py`), also install:
51
+
52
+ ```bash
53
+ pip install pyyaml
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Authentication
59
+
60
+ ```python
61
+ from codex_backend_sdk import CodexClient
62
+
63
+ client = CodexClient().authenticate()
64
+ ```
65
+
66
+ `authenticate()` handles everything automatically:
67
+ - **Tokens present and fresh** → used directly, no network call
68
+ - **Tokens stale** → silently refreshed in the background
69
+ - **No valid tokens available** → opens your browser for the OAuth flow (blocking, first run only)
70
+
71
+ Tokens are saved to `~/.codex/auth.json` (created if it doesn't exist). If the official Codex CLI is also installed, both share the same file.
72
+
73
+ All other methods (`stream()`, `respond()`, `list_models()`, …) raise immediately if `authenticate()` was not called — they never trigger the OAuth flow implicitly.
74
+
75
+ ---
76
+
77
+ ## Basic usage
78
+
79
+ ```python
80
+ from codex_backend_sdk import CodexClient, TextDelta, ResponseCompleted
81
+
82
+ client = CodexClient().authenticate()
83
+
84
+ for event in client.stream("Explain quicksort in one paragraph"):
85
+ if isinstance(event, TextDelta):
86
+ print(event.text, end="", flush=True)
87
+ elif isinstance(event, ResponseCompleted):
88
+ print(f"\n[tokens: in={event.input_tokens} out={event.output_tokens}]")
89
+ ```
90
+
91
+ Or collect the full response at once:
92
+
93
+ ```python
94
+ text, completion = client.respond("Explain quicksort in one paragraph")
95
+ print(text)
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Aborting a stream
101
+
102
+ `CodexClient.abort()` stops the currently active streaming response. The stream
103
+ iterator raises `ResponseAborted`, while captured output emitted before the abort
104
+ remains available to your caller.
105
+
106
+ ```python
107
+ from codex_backend_sdk import CodexClient, ResponseAborted, TextDelta
108
+
109
+ client = CodexClient().authenticate()
110
+ events = client.stream("Write a long essay")
111
+
112
+ try:
113
+ for event in events:
114
+ if isinstance(event, TextDelta):
115
+ print(event.text, end="", flush=True)
116
+ if should_stop():
117
+ client.abort()
118
+ except ResponseAborted:
119
+ pass
120
+ ```
121
+
122
+ Calling `abort()` when no stream is active is a no-op.
123
+
124
+ ---
125
+
126
+ ## Models
127
+
128
+ ```python
129
+ models = client.list_models()
130
+ for m in models:
131
+ print(m.slug, m.display_name, m.context_window)
132
+
133
+ info = client.get_model("codex-mini-latest")
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Multi-turn conversation
139
+
140
+ Pass prior turns as `conversation_history`. Each turn is a raw dict in Responses API format.
141
+
142
+ ```python
143
+ history = []
144
+
145
+ def chat(user_input: str) -> str:
146
+ text, _ = client.respond(user_input, conversation_history=history)
147
+ history.append({
148
+ "type": "message", "role": "user",
149
+ "content": [{"type": "input_text", "text": user_input}],
150
+ })
151
+ history.append({
152
+ "type": "message", "role": "assistant",
153
+ "content": [{"type": "output_text", "text": text}],
154
+ })
155
+ return text
156
+
157
+ print(chat("My name is Alice."))
158
+ print(chat("What's my name?")) # model remembers
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Tool use
164
+
165
+ Tool definitions follow the same format as the official OpenAI SDK.
166
+
167
+ ```python
168
+ import json
169
+ from codex_backend_sdk import CodexClient, TextDelta, ToolCall, ResponseCompleted
170
+
171
+ client = CodexClient().authenticate()
172
+
173
+ tools = [
174
+ {
175
+ "type": "function",
176
+ "name": "get_weather",
177
+ "description": "Get the current temperature for a city.",
178
+ "parameters": {
179
+ "type": "object",
180
+ "properties": {
181
+ "city": {"type": "string", "description": "City name"},
182
+ },
183
+ "required": ["city"],
184
+ },
185
+ }
186
+ ]
187
+
188
+ def get_weather(city: str) -> dict:
189
+ return {"city": city, "temperature": 22, "unit": "celsius"}
190
+
191
+
192
+ history = []
193
+
194
+ # Turn 1 — model may emit a ToolCall
195
+ for event in client.stream("What's the weather in Paris?", tools=tools):
196
+ if isinstance(event, TextDelta):
197
+ print(event.text, end="", flush=True)
198
+ elif isinstance(event, ToolCall):
199
+ result = get_weather(**event.parsed_arguments())
200
+ history.append(event.as_history_item()) # the function_call item
201
+ history.append(event.to_tool_result(json.dumps(result))) # function_call_output
202
+
203
+ # Turn 2 — model sees the tool result and replies
204
+ for event in client.stream(None, conversation_history=history, tools=tools):
205
+ if isinstance(event, TextDelta):
206
+ print(event.text, end="", flush=True)
207
+ elif isinstance(event, ResponseCompleted):
208
+ print()
209
+ ```
210
+
211
+ `tool_choice` defaults to `"auto"`. Other values: `"none"`, `"required"`, or `{"type": "function", "name": "..."}`.
212
+
213
+ ---
214
+
215
+ ## Image input
216
+
217
+ ```python
218
+ from codex_backend_sdk import image_url, image_b64
219
+
220
+ # From a URL
221
+ for event in client.stream(
222
+ ["What's in this image?", image_url("https://example.com/photo.jpg")]
223
+ ):
224
+ ...
225
+
226
+ # From a local file (base64)
227
+ import base64
228
+ with open("photo.jpg", "rb") as f:
229
+ data = base64.b64encode(f.read()).decode()
230
+
231
+ for event in client.stream(
232
+ ["Describe this image.", image_b64(data, "image/jpeg")]
233
+ ):
234
+ ...
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Reasoning
240
+
241
+ ```python
242
+ # Enable chain-of-thought (reasoning tokens are billed separately)
243
+ for event in client.stream(
244
+ "Solve: if 3x + 7 = 22, what is x?",
245
+ reasoning="medium",
246
+ reasoning_summary="concise", # "concise" | "detailed" | "auto"
247
+ ):
248
+ ...
249
+ ```
250
+
251
+ `reasoning` values: `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`.
252
+
253
+ ---
254
+
255
+ ## Structured output (JSON Schema)
256
+
257
+ ```python
258
+ import json
259
+
260
+ schema = {
261
+ "title": "person",
262
+ "type": "object",
263
+ "properties": {
264
+ "name": {"type": "string"},
265
+ "age": {"type": "integer"},
266
+ },
267
+ "required": ["name", "age"],
268
+ "additionalProperties": False,
269
+ }
270
+
271
+ text, _ = client.respond(
272
+ "Extract: Alice is 30 years old.",
273
+ output_schema=schema,
274
+ )
275
+ person = json.loads(text)
276
+ print(person) # {"name": "Alice", "age": 30}
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Context compaction
282
+
283
+ When a conversation grows long, compact it into an encrypted summary the model can still read:
284
+
285
+ ```python
286
+ result = client.compact(history)
287
+
288
+ # result.output_items replaces the full history
289
+ for event in client.stream("Continue…", conversation_history=result.output_items):
290
+ ...
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Usage / quota
296
+
297
+ ```python
298
+ quota = client.usage()
299
+ print(quota) # raw dict from /backend-api/wham/usage
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Stream events reference
305
+
306
+ | Type | Emitted when |
307
+ |---|---|
308
+ | `TextDelta` | incremental text chunk arrives |
309
+ | `ReasoningDelta` | reasoning summary chunk (requires `include_reasoning=True`) |
310
+ | `ToolCall` | model requests a function call |
311
+ | `OutputItem` | a non-tool output item completes (message, compaction_summary, …) |
312
+ | `ResponseCompleted` | stream ends successfully; carries full `TokenUsage` |
313
+ | `ResponseFailed` | stream ends with an error (`code`, `message`) |
314
+ | `ResponseAborted` | exception raised when the caller aborts an active stream |
315
+
316
+ ---
317
+
318
+ ## `stream()` parameters
319
+
320
+ | Parameter | Type | Default | Description |
321
+ |---|---|---|---|
322
+ | `user_message` | `str \| list[dict] \| None` | — | User prompt. `None` to continue without a new message (e.g. after tool results). |
323
+ | `model` | `str` | `"gpt-5.4"` | Model slug. |
324
+ | `instructions` | `str` | `""` | System instructions. |
325
+ | `conversation_history` | `list[dict]` | `None` | Prior turns (ResponseItem format). |
326
+ | `tools` | `list[dict]` | `None` | Tool definitions (OpenAI function format). |
327
+ | `tool_choice` | `str \| dict` | `"auto"` | `"auto"`, `"none"`, `"required"`, or `{"type":"function","name":"..."}`. |
328
+ | `parallel_tool_calls` | `bool` | `False` | Allow multiple tool calls in one turn. |
329
+ | `reasoning` | `str` | `None` | `"minimal"` / `"low"` / `"medium"` / `"high"` / `"xhigh"`. |
330
+ | `reasoning_summary` | `str` | `None` | `"concise"` / `"detailed"` / `"auto"`. |
331
+ | `verbosity` | `str` | `None` | `"low"` / `"medium"` / `"high"`. Mutually exclusive with `output_schema`. |
332
+ | `output_schema` | `dict` | `None` | JSON Schema for structured output. |
333
+ | `include_reasoning` | `bool` | `False` | Emit `ReasoningDelta` events. |
334
+ | `web_search` | `str` | `None` | `"cached"` (OpenAI index), `"live"` (real-time fetch), or `"disabled"` / `None` (off). Incompatible with `reasoning="minimal"`. |
335
+ | `store` | `bool` | `False` | Persist the response server-side. |
336
+ | `service_tier` | `str` | `None` | `"flex"` (higher throughput) or `"fast"` (lower latency). |
337
+ | `prompt_cache_key` | `str` | `None` | UUID to share across calls with a common prefix — hits the server-side prompt cache. |
338
+
339
+ ---
340
+
341
+ ## How it works
342
+
343
+ Authentication uses the same ChatGPT OAuth 2.0 + PKCE flow as the official Codex CLI (`codex-rs`). Tokens are stored in `~/.codex/auth.json` and can be refreshed without re-opening the browser.
344
+
345
+ The backend (`chatgpt.com/backend-api/codex`) is distinct from `api.openai.com` and is accessed via your ChatGPT subscription rather than an API key. All responses are streamed over SSE — the backend does not support non-streaming requests.
346
+
347
+ ### Realtime WebRTC
348
+
349
+ `/backend-api/codex/realtime/calls` currently returns `404`, but the WebRTC call
350
+ creation route used by Codex works at `https://api.openai.com/v1/realtime/calls`
351
+ with the saved ChatGPT OAuth bearer token and `ChatGPT-Account-ID`.
352
+
353
+ Generate an SDP offer in a browser or WebView with an audio transceiver and the
354
+ `oai-events` data channel, then create the call:
355
+
356
+ ```python
357
+ result = client.create_realtime_call(
358
+ offer_sdp,
359
+ instructions="You are concise.",
360
+ session_id="thread-or-session-id",
361
+ )
362
+
363
+ print(result.answer_sdp)
364
+ print(result.call_id)
365
+ print(result.sideband_url)
366
+ ```
367
+
368
+ This path intentionally does not read `OPENAI_API_KEY`; it uses the ChatGPT auth
369
+ tokens loaded from `~/.codex/auth.json`.