From c79e85e6351a2ecf66e2f86914182899ae6d1a72 Mon Sep 17 00:00:00 2001 From: yunyaozhou Date: Fri, 10 Jul 2026 07:29:01 +0800 Subject: [PATCH] fix: tolerate gateways without image profiles --- codex_retry_gateway_tui.py | 21 ++++++++++++++++----- pyproject.toml | 2 +- tests/test_payload.py | 18 ++++++++++++++++++ uv.lock | 2 +- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index 2c848b6..2689fcb 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -16,6 +16,7 @@ import subprocess import sys import tempfile import tomllib +import urllib.error import urllib.parse import urllib.request from pathlib import Path @@ -23,7 +24,7 @@ from typing import Any APP_NAME = "codex-retry-gateway-tui" -FALLBACK_VERSION = "0.1.8" +FALLBACK_VERSION = "0.1.9" DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway" DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway" DEFAULT_API_URL = DEFAULT_GATEWAY_URL @@ -1150,11 +1151,21 @@ def fetch_payload(api_url: str, timeout: int, access_key: str = "") -> dict[str, return data -def fetch_optional_payload(url: str, timeout: int, access_key: str = "") -> tuple[dict[str, Any], str]: +def fetch_optional_payload( + url: str, + timeout: int, + access_key: str = "", + *, + allow_not_found: bool = False, +) -> tuple[dict[str, Any], str]: if not str(url or "").strip(): return {}, "" try: return fetch_payload(url, timeout, access_key), "" + except urllib.error.HTTPError as exc: + if allow_not_found and exc.code == 404: + return {}, "" + return {}, str(exc) except Exception as exc: return {}, str(exc) @@ -2030,12 +2041,12 @@ def fetch_dashboard_snapshot( "image_profiles": build_api_url(gateway_root, "/api/image-profiles"), } - def fetch_optional(url: str) -> tuple[dict[str, Any], str]: - return fetch_optional_payload(url, timeout, access_key) + def fetch_optional(url: str, *, allow_not_found: bool = False) -> tuple[dict[str, Any], str]: + return fetch_optional_payload(url, timeout, access_key, allow_not_found=allow_not_found) with ThreadPoolExecutor(max_workers=5) as pool: futures = { - name: pool.submit(fetch_optional, url) + name: pool.submit(fetch_optional, url, allow_not_found=name == "image_profiles") for name, url in endpoints.items() } status_payload, status_error = futures["status"].result() diff --git a/pyproject.toml b/pyproject.toml index 7adae8a..b1219f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codex-retry-gateway-tui" -version = "0.1.8" +version = "0.1.9" description = "Terminal UI for codex-retry-gateway monitoring and control" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_payload.py b/tests/test_payload.py index 7fd5a3c..5c67770 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -121,6 +121,24 @@ class CodexRetryGatewayTUITests(unittest.TestCase): self.assertEqual(mod.request_updated_elapsed(row), "1.0s") self.assertEqual(mod.request_chunk_progress(row), "7 / 4.1KB") + def test_optional_image_profiles_endpoint_can_be_absent_on_older_gateway(self) -> None: + mod = load_module() + missing_endpoint = mod.urllib.error.HTTPError( + "http://127.0.0.1:4610/__codex_retry_gateway/api/image-profiles", + 404, + "Not Found", + {}, + None, + ) + with mock.patch.object(mod, "fetch_payload", side_effect=missing_endpoint): + payload, error = mod.fetch_optional_payload( + "http://127.0.0.1:4610/__codex_retry_gateway/api/image-profiles", + 5, + allow_not_found=True, + ) + self.assertEqual(payload, {}) + self.assertEqual(error, "") + def test_request_usage_summary_shows_cached_ratio(self) -> None: mod = load_module() row = { diff --git a/uv.lock b/uv.lock index 739c1c0..12ddf6e 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "codex-retry-gateway-tui" -version = "0.1.8" +version = "0.1.9" source = { editable = "." } dependencies = [ { name = "textual" },