codex-backend-sdk 0.3.3__tar.gz → 0.3.5__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.5/CHANGELOG.md +21 -0
  2. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/PKG-INFO +9 -1
  3. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/README.md +8 -0
  4. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/__init__.py +1 -1
  5. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/_client.py +25 -1
  6. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/_models.py +1 -0
  7. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/_responses_payloads.py +1 -7
  8. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/responses.py +7 -1
  9. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/pyproject.toml +1 -1
  10. codex_backend_sdk-0.3.5/tests/test_account_info.py +68 -0
  11. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_responses_resource.py +6 -1
  12. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/.gitignore +0 -0
  13. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/LICENSE +0 -0
  14. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/_streaming.py +0 -0
  15. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/_transport.py +0 -0
  16. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/_utils.py +0 -0
  17. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/codex_client.py +0 -0
  18. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/oauth.py +0 -0
  19. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/pkce.py +0 -0
  20. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/__init__.py +0 -0
  21. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/codex.py +0 -0
  22. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/files.py +0 -0
  23. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/models.py +0 -0
  24. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/openai_oauth.py +0 -0
  25. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/resources/realtime.py +0 -0
  26. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/codex_backend_sdk/storage.py +0 -0
  27. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/docs/backend-api.md +0 -0
  28. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/examples/agent.py +0 -0
  29. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/conftest.py +0 -0
  30. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_basic.py +0 -0
  31. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_client_retry.py +0 -0
  32. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_codex_resources.py +0 -0
  33. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_conversation.py +0 -0
  34. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_files_resource.py +0 -0
  35. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_openai_oauth_resources.py +0 -0
  36. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_realtime_resource.py +0 -0
  37. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_reasoning.py +0 -0
  38. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_structured_output.py +0 -0
  39. {codex_backend_sdk-0.3.3 → codex_backend_sdk-0.3.5}/tests/test_tools.py +0 -0
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.3.5] - 2026-05-18
6
+
7
+ ### Fixed
8
+ - Preserved `usage` on `client.responses.compact(...)` results so `CompactedResponse` exposes backend token accounting.
9
+
10
+ ### Tests
11
+ - Added coverage to verify compact responses retain `input_tokens`, `output_tokens`, and `total_tokens`.
12
+
13
+ ## [0.3.4] - 2026-05-15
14
+
15
+ ### Added
16
+ - Added `authenticate(interactive=False)` to allow non-interactive credential checks without triggering the browser OAuth flow.
17
+ - Added `client.authenticated` for simple auth-state introspection.
18
+ - Added `client.account_info()` to expose safe non-secret account metadata (`authenticated`, `account_id`, `email`, `plan_type`).
19
+
20
+ ### Documentation
21
+ - 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.3
3
+ Version: 0.3.5
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -355,6 +355,14 @@ headers = client.realtime_websocket_headers(session_id="voice-session")
355
355
  `openai_api_key`. The default `authenticate(request_api_key=True)` flow stores
356
356
  that key when available.
357
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
+
358
366
  ### Embeddings
359
367
 
360
368
  `client.embeddings.create(...)` mirrors the official OpenAI embeddings resource
@@ -329,6 +329,14 @@ headers = client.realtime_websocket_headers(session_id="voice-session")
329
329
  `openai_api_key`. The default `authenticate(request_api_key=True)` flow stores
330
330
  that key when available.
331
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
+
332
340
  ### Embeddings
333
341
 
334
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.3"
20
+ __version__ = "0.3.5"
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
@@ -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())
@@ -258,6 +258,7 @@ class CompactedResponse(CodexBaseModel):
258
258
  id: str
259
259
  object: str = "response.compacted"
260
260
  output: list[dict[str, Any]] = Field(default_factory=list)
261
+ usage: Optional[ResponseUsage] = Field(default_factory=ResponseUsage)
261
262
 
262
263
 
263
264
  class BinaryResponseContent:
@@ -175,13 +175,7 @@ def normalize_input_item(item: Any) -> dict[str, Any]:
175
175
  def normalize_tools(tools: Any) -> list[dict[str, Any]]:
176
176
  if not _is_given(tools) or tools is None:
177
177
  return []
178
- normalized = []
179
- for tool in tools:
180
- item = _as_dict(tool)
181
- if item.get("type") == "web_search":
182
- item["type"] = "web_search_preview"
183
- normalized.append(item)
184
- return normalized
178
+ return [_as_dict(tool) for tool in tools]
185
179
 
186
180
 
187
181
  def normalize_reasoning(reasoning: Any) -> dict[str, Any]:
@@ -10,6 +10,7 @@ from .._streaming import stream_response_events
10
10
  from .._utils import _UNSET, _default, _is_given, _reject_backend_unsupported
11
11
  from ._responses_payloads import (
12
12
  ResponsesCreateRequest,
13
+ _usage_from_backend,
13
14
  collect_response,
14
15
  merge_text_format,
15
16
  normalize_input_item,
@@ -221,4 +222,9 @@ class Responses:
221
222
  if _is_given(text) and text is not None:
222
223
  payload["text"] = normalize_text(text)
223
224
  data = self._client._post("/responses/compact", body=payload).json()
224
- return CompactedResponse(id=data.get("id", ""), output=data.get("output", []))
225
+ return CompactedResponse(
226
+ id=data.get("id", ""),
227
+ object=data.get("object", "response.compacted"),
228
+ output=data.get("output", []),
229
+ usage=_usage_from_backend(data.get("usage")),
230
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-backend-sdk"
7
- version = "0.3.3"
7
+ version = "0.3.5"
8
8
  description = "Unofficial Python SDK for the ChatGPT Codex backend API"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,68 @@
1
+ from codex_backend_sdk import CodexClient, TokenStore
2
+
3
+
4
+
5
+ def _store() -> TokenStore:
6
+ return TokenStore(
7
+ access_token="access-secret",
8
+ refresh_token="refresh-secret",
9
+ id_token_raw="id-secret",
10
+ account_id="acct_123",
11
+ openai_api_key="api-secret",
12
+ email="user@example.com",
13
+ plan_type="pro",
14
+ )
15
+
16
+
17
+
18
+ def test_account_info_exposes_safe_metadata_only():
19
+ client = CodexClient(store=_store())
20
+
21
+ info = client.account_info()
22
+
23
+ assert client.authenticated is True
24
+ assert info == {
25
+ "authenticated": True,
26
+ "account_id": "acct_123",
27
+ "email": "user@example.com",
28
+ "plan_type": "pro",
29
+ }
30
+ assert "access-secret" not in repr(info)
31
+ assert "refresh-secret" not in repr(info)
32
+ assert "api-secret" not in repr(info)
33
+
34
+
35
+
36
+ def test_account_info_when_not_authenticated():
37
+ client = CodexClient()
38
+
39
+ assert client.authenticated is False
40
+ assert client.account_info() == {
41
+ "authenticated": False,
42
+ "account_id": None,
43
+ "email": None,
44
+ "plan_type": None,
45
+ }
46
+
47
+
48
+
49
+ def test_authenticate_non_interactive_uses_loaded_tokens(monkeypatch):
50
+ monkeypatch.setattr("codex_backend_sdk._client.load_tokens", lambda: _store())
51
+ monkeypatch.setattr("codex_backend_sdk._client.token_needs_refresh", lambda store: False)
52
+
53
+ client = CodexClient().authenticate(interactive=False)
54
+
55
+ assert client.authenticated is True
56
+ assert client.account_info()["email"] == "user@example.com"
57
+
58
+
59
+
60
+ def test_authenticate_non_interactive_without_tokens_raises(monkeypatch):
61
+ monkeypatch.setattr("codex_backend_sdk._client.load_tokens", lambda: None)
62
+
63
+ try:
64
+ CodexClient().authenticate(interactive=False)
65
+ except RuntimeError as exc:
66
+ assert "interactive login required" in str(exc)
67
+ else:
68
+ raise AssertionError("Expected RuntimeError")
@@ -39,6 +39,7 @@ class FakeClient(OpenAI):
39
39
  return FakeJSONResponse({
40
40
  "id": "resp_123",
41
41
  "output": [{"type": "message", "content": []}],
42
+ "usage": {"input_tokens": 11, "output_tokens": 7, "total_tokens": 18},
42
43
  })
43
44
  return FakeSSE([
44
45
  'data: {"type":"response.content_part.delta","delta":{"text":"hel"}}',
@@ -282,6 +283,10 @@ def test_responses_compact_sends_shared_request_fields():
282
283
  )
283
284
 
284
285
  assert compacted.id == "resp_123"
286
+ assert compacted.usage is not None
287
+ assert compacted.usage.input_tokens == 11
288
+ assert compacted.usage.output_tokens == 7
289
+ assert compacted.usage.total_tokens == 18
285
290
  path, payload, stream = client.posts[0]
286
291
  assert path == "/responses/compact"
287
292
  assert stream is False
@@ -295,7 +300,7 @@ def test_responses_compact_sends_shared_request_fields():
295
300
  "content": [{"type": "input_text", "text": "Long context"}],
296
301
  }
297
302
  ],
298
- "tools": [{"type": "web_search_preview"}],
303
+ "tools": [{"type": "web_search"}],
299
304
  "parallel_tool_calls": True,
300
305
  "reasoning": {"effort": "medium"},
301
306
  "service_tier": "priority",