codex-backend-sdk 0.5.6__tar.gz → 0.5.7__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 (78) hide show
  1. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/CHANGELOG.md +9 -0
  2. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/PKG-INFO +19 -1
  3. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/README.md +18 -0
  4. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/__init__.py +1 -1
  5. codex_backend_sdk-0.5.7/codex_backend_sdk/_codex_version.py +45 -0
  6. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/models.py +7 -7
  7. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/docs/api-reference.md +1 -1
  8. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/docs/backend-api.md +5 -1
  9. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/pyproject.toml +1 -1
  10. codex_backend_sdk-0.5.7/tests/test_codex_version.py +88 -0
  11. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_responses_resource.py +28 -1
  12. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/uv.lock +1 -1
  13. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/.gitignore +0 -0
  14. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/LICENSE +0 -0
  15. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/_client.py +0 -0
  16. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/_models.py +0 -0
  17. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/_streaming.py +0 -0
  18. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/_transport.py +0 -0
  19. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/_utils.py +0 -0
  20. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/codex_client.py +0 -0
  21. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/oauth.py +0 -0
  22. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/pkce.py +0 -0
  23. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/__init__.py +0 -0
  24. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/_responses_payloads.py +0 -0
  25. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/_responses_routing.py +0 -0
  26. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/chatgpt.py +0 -0
  27. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/chatgpt_apps.py +0 -0
  28. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/chatgpt_connectors.py +0 -0
  29. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/chatgpt_plugin_bundles.py +0 -0
  30. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/chatgpt_plugin_shares.py +0 -0
  31. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/chatgpt_plugins.py +0 -0
  32. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/chatgpt_writing_blocks.py +0 -0
  33. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/codex.py +0 -0
  34. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/codex_web_search.py +0 -0
  35. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/files.py +0 -0
  36. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/images.py +0 -0
  37. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/openai_oauth.py +0 -0
  38. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/realtime.py +0 -0
  39. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/remote_control.py +0 -0
  40. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/responses.py +0 -0
  41. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/responses_websocket.py +0 -0
  42. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/resources/worktree_snapshots.py +0 -0
  43. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/codex_backend_sdk/storage.py +0 -0
  44. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/docs/desktop-endpoint-inventory.md +0 -0
  45. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/docs/endpoint-coverage.md +0 -0
  46. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/docs/openai-compatibility.md +0 -0
  47. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/examples/agent.py +0 -0
  48. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/conftest.py +0 -0
  49. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_account_info.py +0 -0
  50. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_basic.py +0 -0
  51. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_apps.py +0 -0
  52. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_connectors.py +0 -0
  53. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_plugin_bundles.py +0 -0
  54. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_plugin_shares.py +0 -0
  55. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_plugins.py +0 -0
  56. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_projects_gizmos.py +0 -0
  57. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_resources.py +0 -0
  58. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_search_files.py +0 -0
  59. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_chatgpt_writing_blocks.py +0 -0
  60. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_client_retry.py +0 -0
  61. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_codex_resources.py +0 -0
  62. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_codex_web_search.py +0 -0
  63. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_conversation.py +0 -0
  64. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_files_resource.py +0 -0
  65. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_hosted_mcp.py +0 -0
  66. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_images.py +0 -0
  67. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_oauth.py +0 -0
  68. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_openai_compatibility.py +0 -0
  69. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_openai_oauth_resources.py +0 -0
  70. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_prompt_cache_usage.py +0 -0
  71. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_realtime_resource.py +0 -0
  72. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_reasoning.py +0 -0
  73. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_remote_control_resource.py +0 -0
  74. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_responses_routing.py +0 -0
  75. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_responses_websocket.py +0 -0
  76. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_structured_output.py +0 -0
  77. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_tools.py +0 -0
  78. {codex_backend_sdk-0.5.6 → codex_backend_sdk-0.5.7}/tests/test_worktree_snapshots.py +0 -0
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.5.7] - 2026-10-22
6
+
7
+ ### Fixed
8
+ - Discover the latest stable Codex CLI version over credential-free HTTPS for
9
+ model catalogs instead of sending a stale fixed version that can hide models.
10
+ Share a 24-hour process cache, retain a verified fallback on registry failure,
11
+ and allow an explicit `extra_query` version without registry access.
12
+ - Keep catalogs fetched with custom request options out of the default cache.
13
+
5
14
  ## [0.5.6] - 2026-10-22
6
15
 
7
16
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codex-backend-sdk
3
- Version: 0.5.6
3
+ Version: 0.5.7
4
4
  Summary: Unofficial Python SDK for the ChatGPT Codex backend API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -278,6 +278,24 @@ for model in client.models.list():
278
278
  model = client.models.retrieve("gpt-5.5")
279
279
  ```
280
280
 
281
+ Model discovery obtains the latest stable Codex CLI version over HTTPS from the
282
+ public npm registry; Node, npm and the Codex binary are not required. The version
283
+ is cached for 24 hours per process, shared across clients. The lookup uses a
284
+ separate request without backend credentials and a three-second timeout. On
285
+ failure it keeps the last known version (or a bundled verified fallback) and
286
+ retries no sooner than five minutes later. This only updates the advertised
287
+ catalog version, not the SDK's implementation of backend features.
288
+
289
+ To pin the catalog version and skip registry access entirely:
290
+
291
+ ```python
292
+ models = client.models.list(extra_query={"client_version": "0.159.2"})
293
+ ```
294
+
295
+ `force_refresh=True` bypasses the five-minute model catalog cache, not the
296
+ 24-hour version cache. Requests with explicit options do not replace the default
297
+ catalog cache. Other protocol and application version fields are independent.
298
+
281
299
  ### Upload a file
282
300
 
283
301
  ```python
@@ -249,6 +249,24 @@ for model in client.models.list():
249
249
  model = client.models.retrieve("gpt-5.5")
250
250
  ```
251
251
 
252
+ Model discovery obtains the latest stable Codex CLI version over HTTPS from the
253
+ public npm registry; Node, npm and the Codex binary are not required. The version
254
+ is cached for 24 hours per process, shared across clients. The lookup uses a
255
+ separate request without backend credentials and a three-second timeout. On
256
+ failure it keeps the last known version (or a bundled verified fallback) and
257
+ retries no sooner than five minutes later. This only updates the advertised
258
+ catalog version, not the SDK's implementation of backend features.
259
+
260
+ To pin the catalog version and skip registry access entirely:
261
+
262
+ ```python
263
+ models = client.models.list(extra_query={"client_version": "0.159.2"})
264
+ ```
265
+
266
+ `force_refresh=True` bypasses the five-minute model catalog cache, not the
267
+ 24-hour version cache. Requests with explicit options do not replace the default
268
+ catalog cache. Other protocol and application version fields are independent.
269
+
252
270
  ### Upload a file
253
271
 
254
272
  ```python
@@ -17,7 +17,7 @@ Quickstart:
17
17
  print(response.output_text)
18
18
  """
19
19
 
20
- __version__ = "0.5.6"
20
+ __version__ = "0.5.7"
21
21
 
22
22
  from .oauth import (
23
23
  DeviceCode,
@@ -0,0 +1,45 @@
1
+ """Shared discovery of the upstream Codex CLI version (not the SDK version)."""
2
+
3
+ import re
4
+ import threading
5
+ import time
6
+
7
+ import requests
8
+
9
+
10
+ # Last verified upstream protocol version, used when discovery is unavailable.
11
+ CLIENT_VERSION = "0.163.0"
12
+ _RETRY_TTL = 300
13
+ _VERSION_TTL = 24 * 60 * 60
14
+ _VERSION_URL = "https://registry.npmjs.org/@openai/codex/latest"
15
+ _version = CLIENT_VERSION
16
+ _version_refresh_at = 0.0
17
+ _version_lock = threading.Lock()
18
+
19
+
20
+ def codex_client_version() -> str:
21
+ """Discover the stable CLI version without sharing backend credentials.
22
+
23
+ Cache across clients in this process. Failed refreshes retain the last known
24
+ version and wait five minutes before retrying, including on first use.
25
+ """
26
+ global _version, _version_refresh_at
27
+ with _version_lock:
28
+ if time.monotonic() < _version_refresh_at:
29
+ return _version
30
+ ttl = _VERSION_TTL
31
+ try:
32
+ with requests.get(_VERSION_URL, timeout=3) as response:
33
+ response.raise_for_status()
34
+ payload = response.json()
35
+ candidate = payload.get("version") if isinstance(payload, dict) else None
36
+ if not isinstance(candidate, str) or not re.fullmatch(
37
+ r"[0-9]+\.[0-9]+\.[0-9]+", candidate
38
+ ):
39
+ raise ValueError("Invalid stable Codex version")
40
+ _version = candidate
41
+ except (requests.RequestException, ValueError):
42
+ ttl = _RETRY_TTL
43
+ _version_refresh_at = time.monotonic() + ttl
44
+ return _version
45
+
@@ -5,18 +5,15 @@ from __future__ import annotations
5
5
  import time
6
6
  from typing import Any, TYPE_CHECKING
7
7
 
8
+ from .._codex_version import codex_client_version
8
9
  from .._models import Model, SyncPage
9
10
  from .._utils import _UNSET, CodexBackendUnsupportedParameterError, _is_given
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from .._client import CodexClient
13
14
 
14
- # This is the upstream Codex CLI protocol/client version expected by the
15
- # ChatGPT backend, not the PyPI package version.
16
- CLIENT_VERSION = "0.130.0"
17
15
  _CACHE_TTL = 300
18
16
 
19
-
20
17
  class Models:
21
18
  def __init__(self, client: CodexClient) -> None:
22
19
  self._client = client
@@ -41,7 +38,9 @@ class Models:
41
38
  if time.time() - self._cache_fetched_at <= _CACHE_TTL:
42
39
  return self._cache
43
40
 
44
- params = {"client_version": CLIENT_VERSION, **(extra_query or {})}
41
+ params = dict(extra_query or {})
42
+ if "client_version" not in params:
43
+ params["client_version"] = codex_client_version()
45
44
  response = self._client._get_raw(
46
45
  "/models", params=params, headers=extra_headers, timeout=timeout
47
46
  )
@@ -49,8 +48,9 @@ class Models:
49
48
  models = [_model_from_backend(item) for item in data.get("models", [])]
50
49
  models.sort(key=lambda model: getattr(model, "priority", 0))
51
50
  page = SyncPage(data=models, etag=response.headers.get("ETag"))
52
- self._cache = page
53
- self._cache_fetched_at = time.time()
51
+ if not has_request_options:
52
+ self._cache = page
53
+ self._cache_fetched_at = time.time()
54
54
  return page
55
55
 
56
56
  def retrieve(
@@ -175,7 +175,7 @@ concurrent, turns. Backend error envelopes raise `ResponsesWebSocketError` with
175
175
 
176
176
  | Method | Parameters | Returns |
177
177
  | --- | --- | --- |
178
- | `client.models.list(...)` | `force_refresh=False`, `extra_headers=None`, `extra_query=None`, `extra_body=None`, `timeout=UNSET` | iterable `SyncPage` with optional `etag`; cached for 5 minutes |
178
+ | `client.models.list(...)` | `force_refresh=False`, `extra_headers=None`, `extra_query=None`, `extra_body=None`, `timeout=UNSET` | iterable `SyncPage` with optional `etag`; default catalog cached for 5 minutes; Codex version auto-discovered over HTTPS (24-hour process cache), or pinned via `extra_query["client_version"]` |
179
179
  | `client.models.retrieve(...)` | `model`, plus the same transport controls | one typed `Model`; raises `LookupError` if absent |
180
180
 
181
181
  `Model` preserves Codex fields including display metadata, context window,
@@ -78,7 +78,11 @@ List models available to this account.
78
78
  **SDK method**: `client.models.list()` / `client.models.retrieve(model)`
79
79
 
80
80
  **Query params**
81
- - `client_version` — Codex CLI protocol/client version string (e.g. `"0.130.0"`).
81
+ - `client_version` — Codex CLI version string. The SDK discovers the latest stable
82
+ version from the public npm registry over HTTPS (24-hour in-process cache;
83
+ three-second timeout; last-known/bundled fallback with five-minute retry
84
+ backoff). Override with `extra_query={"client_version": "0.159.2"}` to skip
85
+ discovery. No Node/npm installation is required.
82
86
 
83
87
  **Response** — JSON `{ "models": [ ModelObject, … ] }`
84
88
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-backend-sdk"
7
- version = "0.5.6"
7
+ version = "0.5.7"
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,88 @@
1
+ """Offline tests for upstream version discovery; no OAuth or npm binary."""
2
+
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from unittest.mock import MagicMock
5
+
6
+ import pytest
7
+ import requests
8
+
9
+ from codex_backend_sdk import _codex_version as version
10
+
11
+
12
+ @pytest.fixture
13
+ def registry(monkeypatch):
14
+ clock = [100.0]
15
+ monkeypatch.setattr(version.time, "monotonic", lambda: clock[0])
16
+ monkeypatch.setattr(version, "_version", version.CLIENT_VERSION)
17
+ monkeypatch.setattr(version, "_version_refresh_at", 0.0)
18
+ response = MagicMock()
19
+ response.__enter__.return_value = response
20
+ response.json.return_value = {"version": "0.170.0"}
21
+ get = MagicMock(return_value=response)
22
+ monkeypatch.setattr(version.requests, "get", get)
23
+ return clock, get, response
24
+
25
+
26
+ def test_version_uses_standalone_https_and_caches_for_24_hours(registry):
27
+ clock, get, response = registry
28
+ assert version.codex_client_version() == "0.170.0"
29
+ get.assert_called_once_with(version._VERSION_URL, timeout=3)
30
+ response.raise_for_status.assert_called_once()
31
+ response.__exit__.assert_called_once()
32
+ clock[0] += 86400 - 1
33
+ assert version.codex_client_version() == "0.170.0"
34
+ assert get.call_count == 1
35
+ clock[0] += 1
36
+ response.json.return_value = {"version": "0.171.0"}
37
+ assert version.codex_client_version() == "0.171.0"
38
+ assert get.call_count == 2
39
+
40
+
41
+ @pytest.mark.parametrize("error", [requests.Timeout(), requests.HTTPError(), ValueError()])
42
+ def test_first_failure_uses_verified_fallback_and_backs_off(registry, error):
43
+ clock, get, _ = registry
44
+ get.side_effect = error
45
+ assert version.codex_client_version() == version.CLIENT_VERSION
46
+ assert version.codex_client_version() == version.CLIENT_VERSION
47
+ assert get.call_count == 1
48
+ clock[0] += 300
49
+ get.side_effect = None
50
+ assert version.codex_client_version() == "0.170.0"
51
+ assert get.call_count == 2
52
+
53
+
54
+
55
+ @pytest.mark.parametrize("stage", ["raise_for_status", "json"])
56
+ def test_response_errors_close_request_and_use_fallback(registry, stage):
57
+ _, _, response = registry
58
+ error = requests.HTTPError() if stage == "raise_for_status" else ValueError()
59
+ getattr(response, stage).side_effect = error
60
+ assert version.codex_client_version() == version.CLIENT_VERSION
61
+ response.__exit__.assert_called_once()
62
+
63
+
64
+ def test_refresh_failure_keeps_last_success(registry):
65
+ clock, get, _ = registry
66
+ assert version.codex_client_version() == "0.170.0"
67
+ clock[0] += 86400
68
+ get.side_effect = requests.ConnectionError()
69
+ assert version.codex_client_version() == "0.170.0"
70
+ assert version.codex_client_version() == "0.170.0"
71
+ assert get.call_count == 2
72
+
73
+
74
+ @pytest.mark.parametrize("payload", [
75
+ None, [], {}, {"version": 123}, {"version": "0.170.0-beta.1"},
76
+ {"version": "garbage"}, {"version": "0.170.0\n"},
77
+ ])
78
+ def test_invalid_registry_metadata_uses_fallback(registry, payload):
79
+ _, _, response = registry
80
+ response.json.return_value = payload
81
+ assert version.codex_client_version() == version.CLIENT_VERSION
82
+
83
+
84
+ def test_concurrent_clients_share_one_lookup(registry):
85
+ _, get, _ = registry
86
+ with ThreadPoolExecutor(max_workers=8) as pool:
87
+ assert list(pool.map(lambda _: version.codex_client_version(), range(16))) == ["0.170.0"] * 16
88
+ assert get.call_count == 1
@@ -5,6 +5,12 @@ import pytest
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from codex_backend_sdk import OpenAI, ParsedResponse, Response
8
+ from codex_backend_sdk.resources import models as models_resource
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def stable_catalog_version(monkeypatch):
13
+ monkeypatch.setattr(models_resource, "codex_client_version", lambda: "0.163.0")
8
14
 
9
15
 
10
16
  class FakeSSE:
@@ -363,12 +369,33 @@ def test_models_resource_forwards_official_request_options():
363
369
 
364
370
  assert client.gets[1] == (
365
371
  "/models",
366
- {"client_version": "0.130.0", "preview": "1"},
372
+ {"client_version": "0.163.0", "preview": "1"},
367
373
  {"x-test": "header"},
368
374
  19,
369
375
  )
370
376
 
371
377
 
378
+
379
+ def test_explicit_catalog_version_skips_discovery_and_does_not_replace_default_cache(monkeypatch):
380
+ client = FakeClient()
381
+ default = client.models.list()
382
+ def unexpected_lookup():
383
+ raise AssertionError("Explicit version must not contact registry")
384
+ monkeypatch.setattr(models_resource, "codex_client_version", unexpected_lookup)
385
+ custom = client.models.list(extra_query={"client_version": "0.159.2"})
386
+ assert custom is not default
387
+ assert client.gets[-1][1] == {"client_version": "0.159.2"}
388
+ assert client.models.list() is default
389
+
390
+
391
+ def test_forced_catalog_refresh_resolves_version_again(monkeypatch):
392
+ client = FakeClient()
393
+ client.models.list()
394
+ monkeypatch.setattr(models_resource, "codex_client_version", lambda: "0.171.0")
395
+ client.models.list(force_refresh=True)
396
+ assert client.gets[-1][1] == {"client_version": "0.171.0"}
397
+
398
+
372
399
  def test_responses_compact_sends_shared_request_fields():
373
400
  client = FakeClient()
374
401
 
@@ -162,7 +162,7 @@ wheels = [
162
162
 
163
163
  [[package]]
164
164
  name = "codex-backend-sdk"
165
- version = "0.5.6"
165
+ version = "0.5.7"
166
166
  source = { editable = "." }
167
167
  dependencies = [
168
168
  { name = "pydantic" },