codex-backend-sdk 0.3.2__tar.gz → 0.3.4__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 (39) hide show
  1. codex_backend_sdk-0.3.4/CHANGELOG.md +13 -0
  2. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/PKG-INFO +33 -1
  3. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/README.md +32 -0
  4. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/__init__.py +5 -1
  5. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_client.py +35 -41
  6. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_models.py +53 -1
  7. codex_backend_sdk-0.3.4/codex_backend_sdk/_transport.py +71 -0
  8. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_utils.py +3 -0
  9. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/codex_client.py +4 -0
  10. codex_backend_sdk-0.3.2/codex_backend_sdk/resources/responses.py → codex_backend_sdk-0.3.4/codex_backend_sdk/resources/_responses_payloads.py +114 -176
  11. codex_backend_sdk-0.3.4/codex_backend_sdk/resources/responses.py +224 -0
  12. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/docs/backend-api.md +4 -0
  13. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/pyproject.toml +1 -1
  14. codex_backend_sdk-0.3.4/tests/test_account_info.py +68 -0
  15. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_client_retry.py +5 -5
  16. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_responses_resource.py +102 -1
  17. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/.gitignore +0 -0
  18. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/LICENSE +0 -0
  19. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/_streaming.py +0 -0
  20. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/oauth.py +0 -0
  21. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/pkce.py +0 -0
  22. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/__init__.py +0 -0
  23. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/codex.py +0 -0
  24. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/files.py +0 -0
  25. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/models.py +0 -0
  26. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/openai_oauth.py +0 -0
  27. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/resources/realtime.py +0 -0
  28. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/codex_backend_sdk/storage.py +0 -0
  29. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/examples/agent.py +0 -0
  30. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/conftest.py +0 -0
  31. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_basic.py +0 -0
  32. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_codex_resources.py +0 -0
  33. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_conversation.py +0 -0
  34. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_files_resource.py +0 -0
  35. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_openai_oauth_resources.py +0 -0
  36. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_realtime_resource.py +0 -0
  37. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_reasoning.py +0 -0
  38. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_structured_output.py +0 -0
  39. {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.4}/tests/test_tools.py +0 -0
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.3.4] - 2026-05-15
6
+
7
+ ### Added
8
+ - Added `authenticate(interactive=False)` to allow non-interactive credential checks without triggering the browser OAuth flow.
9
+ - Added `client.authenticated` for simple auth-state introspection.
10
+ - Added `client.account_info()` to expose safe non-secret account metadata (`authenticated`, `account_id`, `email`, `plan_type`).
11
+
12
+ ### Documentation
13
+ - Documented the non-interactive authentication helpers in `README.md`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-backend-sdk
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -240,6 +240,30 @@ response = client.responses.create(
240
240
  )
241
241
  ```
242
242
 
243
+ For structured output, `client.responses.parse(...)` accepts a Pydantic model,
244
+ sends it as a strict JSON schema, and returns `ParsedResponse`:
245
+
246
+ ```python
247
+ from pydantic import BaseModel
248
+
249
+
250
+ class Person(BaseModel):
251
+ name: str
252
+ age: int
253
+
254
+
255
+ parsed = client.responses.parse(
256
+ model="gpt-5.4",
257
+ input="Extract: Ada is 37 years old.",
258
+ text_format=Person,
259
+ )
260
+ print(parsed.output_parsed.name)
261
+ ```
262
+
263
+ Collected responses expose convenience properties for common output items:
264
+ `response.output_text`, `response.reasoning_summary`, and
265
+ `response.tool_calls`.
266
+
243
267
  Unsupported official Responses parameters are rejected explicitly with
244
268
  `CodexBackendUnsupportedParameterError`, including `temperature`, `top_p`,
245
269
  `max_output_tokens`, `metadata`, `user`, `safety_identifier`, `truncation`,
@@ -331,6 +355,14 @@ headers = client.realtime_websocket_headers(session_id="voice-session")
331
355
  `openai_api_key`. The default `authenticate(request_api_key=True)` flow stores
332
356
  that key when available.
333
357
 
358
+ For non-interactive checks, you can avoid triggering a browser login flow:
359
+
360
+ ```python
361
+ client = OpenAI().authenticate(interactive=False)
362
+ print(client.authenticated)
363
+ print(client.account_info())
364
+ ```
365
+
334
366
  ### Embeddings
335
367
 
336
368
  `client.embeddings.create(...)` mirrors the official OpenAI embeddings resource
@@ -214,6 +214,30 @@ response = client.responses.create(
214
214
  )
215
215
  ```
216
216
 
217
+ For structured output, `client.responses.parse(...)` accepts a Pydantic model,
218
+ sends it as a strict JSON schema, and returns `ParsedResponse`:
219
+
220
+ ```python
221
+ from pydantic import BaseModel
222
+
223
+
224
+ class Person(BaseModel):
225
+ name: str
226
+ age: int
227
+
228
+
229
+ parsed = client.responses.parse(
230
+ model="gpt-5.4",
231
+ input="Extract: Ada is 37 years old.",
232
+ text_format=Person,
233
+ )
234
+ print(parsed.output_parsed.name)
235
+ ```
236
+
237
+ Collected responses expose convenience properties for common output items:
238
+ `response.output_text`, `response.reasoning_summary`, and
239
+ `response.tool_calls`.
240
+
217
241
  Unsupported official Responses parameters are rejected explicitly with
218
242
  `CodexBackendUnsupportedParameterError`, including `temperature`, `top_p`,
219
243
  `max_output_tokens`, `metadata`, `user`, `safety_identifier`, `truncation`,
@@ -305,6 +329,14 @@ headers = client.realtime_websocket_headers(session_id="voice-session")
305
329
  `openai_api_key`. The default `authenticate(request_api_key=True)` flow stores
306
330
  that key when available.
307
331
 
332
+ For non-interactive checks, you can avoid triggering a browser login flow:
333
+
334
+ ```python
335
+ client = OpenAI().authenticate(interactive=False)
336
+ print(client.authenticated)
337
+ print(client.account_info())
338
+ ```
339
+
308
340
  ### Embeddings
309
341
 
310
342
  `client.embeddings.create(...)` mirrors the official OpenAI embeddings resource
@@ -17,7 +17,7 @@ Quickstart:
17
17
  print(response.output_text)
18
18
  """
19
19
 
20
- __version__ = "0.3.2"
20
+ __version__ = "0.3.4"
21
21
 
22
22
  from .oauth import run_oauth_flow, refresh_access_token, obtain_api_key
23
23
  from .storage import load_tokens, save_tokens, TokenStore
@@ -38,7 +38,9 @@ from .codex_client import (
38
38
  Model,
39
39
  RawMemory,
40
40
  RawMemoryMetadata,
41
+ ParsedResponse,
41
42
  Response,
43
+ ResponseFormatJsonSchema,
42
44
  ResponseStreamEvent,
43
45
  ResponseUsage,
44
46
  RealtimeCallResponse,
@@ -61,9 +63,11 @@ __all__ = [
61
63
  "MemorySummarizeOutput",
62
64
  "MemorySummarizeResponse",
63
65
  "Model",
66
+ "ParsedResponse",
64
67
  "RawMemory",
65
68
  "RawMemoryMetadata",
66
69
  "Response",
70
+ "ResponseFormatJsonSchema",
67
71
  "ResponseStreamEvent",
68
72
  "ResponseUsage",
69
73
  "RealtimeCallResponse",
@@ -3,11 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import urllib.parse
6
- import time
7
6
  from typing import Any, Optional
8
7
 
9
8
  import requests
10
9
 
10
+ from ._transport import request_with_retries
11
11
  from ._utils import _UNSET, _is_given
12
12
  from .storage import TokenStore, load_tokens, save_tokens, token_needs_refresh
13
13
 
@@ -62,7 +62,12 @@ class CodexClient:
62
62
  self.files = Files(self)
63
63
  self.codex = CodexResources(self)
64
64
 
65
- def authenticate(self, *, request_api_key: bool = True) -> "CodexClient":
65
+ def authenticate(
66
+ self,
67
+ *,
68
+ request_api_key: bool = True,
69
+ interactive: bool = True,
70
+ ) -> "CodexClient":
66
71
  from .oauth import refresh_access_token, run_oauth_flow
67
72
 
68
73
  def refresh(store: TokenStore) -> Optional[TokenStore]:
@@ -87,9 +92,28 @@ class CodexClient:
87
92
  self._set_store(store)
88
93
  return self
89
94
 
95
+ if not interactive:
96
+ raise RuntimeError("No usable stored Codex credentials; interactive login required.")
97
+
90
98
  self._set_store(run_oauth_flow(request_api_key=request_api_key))
91
99
  return self
92
100
 
101
+ @property
102
+ def authenticated(self) -> bool:
103
+ """Whether this client currently has OAuth credentials loaded."""
104
+ return bool(self._store and self._store.account_id)
105
+
106
+ def account_info(self) -> dict[str, Any]:
107
+ """Return safe non-secret account metadata decoded from stored tokens."""
108
+ store = self._store
109
+ authenticated = bool(store and store.account_id)
110
+ return {
111
+ "authenticated": authenticated,
112
+ "account_id": store.account_id if store else None,
113
+ "email": store.email if store else None,
114
+ "plan_type": store.plan_type if store else None,
115
+ }
116
+
93
117
  def _set_store(self, store: TokenStore) -> None:
94
118
  self._store = store
95
119
  self._session.headers.update(self._auth_headers())
@@ -271,37 +295,16 @@ class CodexClient:
271
295
  return response.json()
272
296
 
273
297
  def _request_with_retries(self, method: str, url: str, **kwargs: Any) -> requests.Response:
274
- last_error: requests.RequestException | None = None
275
298
  use_session = kwargs.pop("_use_session", True)
276
- for attempt in range(self._max_retries + 1):
277
- try:
278
- request = self._session.request if use_session else requests.request
279
- response = request(method, url, **kwargs)
280
- if self._should_retry_response(response, attempt):
281
- self._sleep_before_retry(response, attempt)
282
- continue
283
- response.raise_for_status()
284
- return response
285
- except (requests.Timeout, requests.ConnectionError) as exc:
286
- last_error = exc
287
- if attempt >= self._max_retries:
288
- raise
289
- self._sleep_before_retry(None, attempt)
290
- if last_error is not None:
291
- raise last_error
292
- raise RuntimeError("Request retry loop exhausted")
293
-
294
- def _should_retry_response(self, response: requests.Response, attempt: int) -> bool:
295
- if attempt >= self._max_retries:
296
- return False
297
- return response.status_code == 429 or 500 <= response.status_code < 600
298
-
299
- def _sleep_before_retry(self, response: requests.Response | None, attempt: int) -> None:
300
- retry_after = response.headers.get("Retry-After") if response is not None else None
301
- delay = _parse_retry_after(retry_after)
302
- if delay is None:
303
- delay = self._retry_base_delay * (2 ** attempt)
304
- time.sleep(delay)
299
+ return request_with_retries(
300
+ self._session,
301
+ method,
302
+ url,
303
+ max_retries=self._max_retries,
304
+ retry_base_delay=self._retry_base_delay,
305
+ use_session=use_session,
306
+ **kwargs,
307
+ )
305
308
 
306
309
  def realtime_websocket_url(self, *, model: str) -> str:
307
310
  """Return the official OpenAI Realtime WebSocket URL for Codex plugins."""
@@ -324,12 +327,3 @@ class CodexClient:
324
327
 
325
328
 
326
329
  OpenAI = CodexClient
327
-
328
-
329
- def _parse_retry_after(value: str | None) -> float | None:
330
- if not value:
331
- return None
332
- try:
333
- return max(0.0, float(value))
334
- except ValueError:
335
- return None
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import time
6
6
  from collections.abc import Iterator
7
- from typing import Any, Literal, Optional
7
+ from typing import Any, Generic, Literal, Optional, TypeVar
8
8
 
9
9
  import requests
10
10
  from pydantic import BaseModel, ConfigDict, Field
@@ -13,6 +13,7 @@ ReasoningEffort = Literal["minimal", "low", "medium", "high", "xhigh"]
13
13
  ReasoningSummary = Literal["concise", "detailed", "auto"]
14
14
  Verbosity = Literal["low", "medium", "high"]
15
15
  ServiceTier = Literal["flex", "priority"]
16
+ ParsedT = TypeVar("ParsedT")
16
17
 
17
18
 
18
19
  class CodexBaseModel(BaseModel):
@@ -73,6 +74,14 @@ class ResponseUsage(CodexBaseModel):
73
74
  output_tokens_details: TokenDetails = Field(default_factory=TokenDetails)
74
75
 
75
76
 
77
+ class ResponseFormatJsonSchema(CodexBaseModel):
78
+ type: Literal["json_schema"] = "json_schema"
79
+ name: str
80
+ schema_: dict[str, Any] = Field(alias="schema")
81
+ strict: Optional[bool] = None
82
+ description: Optional[str] = None
83
+
84
+
76
85
  class Response(CodexBaseModel):
77
86
  id: str
78
87
  created_at: float = Field(default_factory=time.time)
@@ -117,6 +126,49 @@ class Response(CodexBaseModel):
117
126
  texts.append(content.get("text", ""))
118
127
  return "".join(texts)
119
128
 
129
+ @property
130
+ def reasoning_summary(self) -> str | None:
131
+ texts: list[str] = []
132
+ for output in self.output:
133
+ if output.get("type") == "reasoning":
134
+ for summary in output.get("summary", []):
135
+ if isinstance(summary, dict):
136
+ texts.append(summary.get("text", ""))
137
+ return "\n".join(text for text in texts if text) or None
138
+
139
+ @property
140
+ def tool_calls(self) -> list[dict[str, Any]]:
141
+ return [output for output in self.output if output.get("type") == "function_call"]
142
+
143
+
144
+ class ParsedResponse(CodexBaseModel, Generic[ParsedT]):
145
+ response: Response
146
+ output_parsed: ParsedT
147
+
148
+ @property
149
+ def id(self) -> str:
150
+ return self.response.id
151
+
152
+ @property
153
+ def model(self) -> Optional[str]:
154
+ return self.response.model
155
+
156
+ @property
157
+ def output(self) -> list[dict[str, Any]]:
158
+ return self.response.output
159
+
160
+ @property
161
+ def output_text(self) -> str:
162
+ return self.response.output_text
163
+
164
+ @property
165
+ def status(self) -> Optional[str]:
166
+ return self.response.status
167
+
168
+ @property
169
+ def usage(self) -> Optional[ResponseUsage]:
170
+ return self.response.usage
171
+
120
172
 
121
173
  class ResponseStreamEvent(CodexBaseModel):
122
174
  type: str
@@ -0,0 +1,71 @@
1
+ """HTTP transport helpers shared by client resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ import requests
9
+
10
+
11
+ def request_with_retries(
12
+ session: requests.Session,
13
+ method: str,
14
+ url: str,
15
+ *,
16
+ max_retries: int,
17
+ retry_base_delay: float,
18
+ use_session: bool = True,
19
+ **kwargs: Any,
20
+ ) -> requests.Response:
21
+ last_error: requests.RequestException | None = None
22
+ for attempt in range(max_retries + 1):
23
+ try:
24
+ request = session.request if use_session else requests.request
25
+ response = request(method, url, **kwargs)
26
+ if should_retry_response(response, attempt, max_retries=max_retries):
27
+ sleep_before_retry(response, attempt, retry_base_delay=retry_base_delay)
28
+ continue
29
+ response.raise_for_status()
30
+ return response
31
+ except (requests.Timeout, requests.ConnectionError) as exc:
32
+ last_error = exc
33
+ if attempt >= max_retries:
34
+ raise
35
+ sleep_before_retry(None, attempt, retry_base_delay=retry_base_delay)
36
+ if last_error is not None:
37
+ raise last_error
38
+ raise RuntimeError("Request retry loop exhausted")
39
+
40
+
41
+ def should_retry_response(
42
+ response: requests.Response,
43
+ attempt: int,
44
+ *,
45
+ max_retries: int,
46
+ ) -> bool:
47
+ if attempt >= max_retries:
48
+ return False
49
+ return response.status_code == 429 or 500 <= response.status_code < 600
50
+
51
+
52
+ def sleep_before_retry(
53
+ response: requests.Response | None,
54
+ attempt: int,
55
+ *,
56
+ retry_base_delay: float,
57
+ ) -> None:
58
+ retry_after = response.headers.get("Retry-After") if response is not None else None
59
+ delay = parse_retry_after(retry_after)
60
+ if delay is None:
61
+ delay = retry_base_delay * (2 ** attempt)
62
+ time.sleep(delay)
63
+
64
+
65
+ def parse_retry_after(value: str | None) -> float | None:
66
+ if not value:
67
+ return None
68
+ try:
69
+ return max(0.0, float(value))
70
+ except ValueError:
71
+ return None
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import base64
6
6
  import json
7
+ from dataclasses import asdict, is_dataclass
7
8
  from typing import Any
8
9
 
9
10
  from pydantic import BaseModel
@@ -26,6 +27,8 @@ def image_b64(data: str, media_type: str = "image/jpeg") -> dict[str, str]:
26
27
  def _jsonable(value: Any) -> Any:
27
28
  if isinstance(value, BaseModel):
28
29
  return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
30
+ if is_dataclass(value) and not isinstance(value, type):
31
+ return _jsonable(asdict(value))
29
32
  if isinstance(value, dict):
30
33
  return {key: _jsonable(item) for key, item in value.items()}
31
34
  if isinstance(value, list):
@@ -16,12 +16,14 @@ from ._models import (
16
16
  Model,
17
17
  MemorySummarizeOutput,
18
18
  MemorySummarizeResponse,
19
+ ParsedResponse,
19
20
  RawMemory,
20
21
  RawMemoryMetadata,
21
22
  ReasoningEffort,
22
23
  ReasoningSummary,
23
24
  RealtimeCallResponse,
24
25
  Response,
26
+ ResponseFormatJsonSchema,
25
27
  ResponseStreamEvent,
26
28
  ResponseUsage,
27
29
  ServiceTier,
@@ -45,11 +47,13 @@ __all__ = [
45
47
  "MemorySummarizeResponse",
46
48
  "Model",
47
49
  "OpenAI",
50
+ "ParsedResponse",
48
51
  "RawMemory",
49
52
  "RawMemoryMetadata",
50
53
  "ReasoningEffort",
51
54
  "ReasoningSummary",
52
55
  "Response",
56
+ "ResponseFormatJsonSchema",
53
57
  "ResponseStreamEvent",
54
58
  "ResponseUsage",
55
59
  "RealtimeCallResponse",