fix: auto-discover local gateway url

This commit is contained in:
2026-06-30 09:57:17 +08:00
parent 99194a8f7d
commit fc88744c50
4 changed files with 115 additions and 13 deletions
+12 -3
View File
@@ -14,13 +14,22 @@ Install as a user command:
```bash ```bash
uv tool install git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git uv tool install git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git
codex-retry-gateway-tui --api-url http://127.0.0.1:4610/__codex_retry_gateway codex-retry-gateway-tui
``` ```
Default gateway URL: Default behavior:
```text ```text
http://127.0.0.1:4610/__codex_retry_gateway 1. reuse saved ~/.config/codex-retry-gateway-tui/api-url when present
2. otherwise auto-discover ~/.codex-retry-gateway/state.json gateway_base_url
3. otherwise fall back to ~/.codex-retry-gateway/config/config.json listen_host/listen_port
4. otherwise use http://127.0.0.1:4610/__codex_retry_gateway
```
Manual override example:
```bash
codex-retry-gateway-tui --api-url http://100.115.235.115:4610
``` ```
Configuration: Configuration:
+60 -9
View File
@@ -19,10 +19,13 @@ from typing import Any
APP_NAME = "codex-retry-gateway-tui" APP_NAME = "codex-retry-gateway-tui"
FALLBACK_VERSION = "0.1.0" FALLBACK_VERSION = "0.1.0"
DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway"
DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway" DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway"
DEFAULT_API_URL = DEFAULT_GATEWAY_URL DEFAULT_API_URL = DEFAULT_GATEWAY_URL
DEFAULT_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/api-url" DEFAULT_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/api-url"
DEFAULT_STATUS_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/status-url" DEFAULT_STATUS_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/status-url"
DEFAULT_GATEWAY_STATE_FILE = "~/.codex-retry-gateway/state.json"
DEFAULT_GATEWAY_JSON_CONFIG_FILE = "~/.codex-retry-gateway/config/config.json"
DEFAULT_VERSION_CHECK_URL = "https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui/raw/branch/main/pyproject.toml" DEFAULT_VERSION_CHECK_URL = "https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui/raw/branch/main/pyproject.toml"
DEFAULT_REFRESH_SECONDS = 10 DEFAULT_REFRESH_SECONDS = 10
DEFAULT_TIMEOUT_SECONDS = 5 DEFAULT_TIMEOUT_SECONDS = 5
@@ -60,14 +63,61 @@ def configured_url(env_names: tuple[str, ...], config_path: str, default: str =
return default return default
def read_json_object(path: str) -> dict[str, Any]:
try:
data = json.loads(Path(path).expanduser().read_text(encoding="utf-8"))
except Exception:
return {}
return data if isinstance(data, dict) else {}
def gateway_admin_url(base_url: str) -> str:
normalized = normalize_gateway_url(base_url)
if not normalized:
return ""
parsed = urllib.parse.urlparse(normalized)
path = parsed.path.rstrip("/")
if not path:
path = DEFAULT_GATEWAY_ADMIN_PATH
elif not path.endswith(DEFAULT_GATEWAY_ADMIN_PATH):
path = f"{path}{DEFAULT_GATEWAY_ADMIN_PATH}"
parsed = parsed._replace(path=path, params="", query="", fragment="")
return urllib.parse.urlunparse(parsed).rstrip("/")
def normalize_listen_host(value: Any) -> str:
host = str(value or "").strip()
if host in {"", "0.0.0.0", "::", "[::]", "*"}:
return "127.0.0.1"
return host
def discover_gateway_url() -> str:
state = read_json_object(DEFAULT_GATEWAY_STATE_FILE)
gateway_base_url = str(state.get("gateway_base_url") or "").strip()
if gateway_base_url:
return gateway_admin_url(gateway_base_url)
config = read_json_object(DEFAULT_GATEWAY_JSON_CONFIG_FILE)
listen_host = normalize_listen_host(config.get("listen_host"))
listen_port = as_int(config.get("listen_port"))
if listen_host and listen_port > 0:
return gateway_admin_url(f"http://{listen_host}:{listen_port}")
return ""
def default_api_url() -> str: def default_api_url() -> str:
return normalize_gateway_url( configured = configured_url(
configured_url( ("CODEX_RETRY_GATEWAY_TUI_API_URL",),
("CODEX_RETRY_GATEWAY_TUI_API_URL",), os.environ.get("CODEX_RETRY_GATEWAY_TUI_API_URL_FILE", DEFAULT_CONFIG_FILE),
os.environ.get("CODEX_RETRY_GATEWAY_TUI_API_URL_FILE", DEFAULT_CONFIG_FILE), "",
DEFAULT_API_URL,
)
) )
if configured:
return gateway_admin_url(configured)
discovered = discover_gateway_url()
if discovered:
return discovered
return gateway_admin_url(DEFAULT_API_URL)
def config_file_path() -> Path: def config_file_path() -> Path:
@@ -128,7 +178,7 @@ def build_api_url(gateway_url: str, suffix: str, params: dict[str, Any] | None =
def write_api_url_config(api_url: str) -> Path: def write_api_url_config(api_url: str) -> Path:
value = str(api_url or "").strip() value = gateway_admin_url(api_url)
if not value: if not value:
raise ValueError("api url is empty") raise ValueError("api url is empty")
path = config_file_path() path = config_file_path()
@@ -639,7 +689,7 @@ def fetch_dashboard_snapshot(
filter_text: str = "", filter_text: str = "",
current_view: str = "overview", current_view: str = "overview",
) -> dict[str, Any]: ) -> dict[str, Any]:
gateway_root = normalize_gateway_url(gateway_url) gateway_root = gateway_admin_url(gateway_url)
endpoints = { endpoints = {
"status": status_url or gateway_status_url(gateway_root), "status": status_url or gateway_status_url(gateway_root),
"logs": build_api_url(gateway_root, "/api/logs", {"limit": 200}), "logs": build_api_url(gateway_root, "/api/logs", {"limit": 200}),
@@ -695,7 +745,7 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
print("Textual is required. Run with: uv run --with textual python codex_retry_gateway_tui.py", file=sys.stderr) print("Textual is required. Run with: uv run --with textual python codex_retry_gateway_tui.py", file=sys.stderr)
return 2 return 2
gateway_base_url = normalize_gateway_url(api_url) gateway_base_url = gateway_admin_url(api_url)
status_api_url = status_url or gateway_status_url(gateway_base_url) status_api_url = status_url or gateway_status_url(gateway_base_url)
class CodexRetryGatewayTui(App[None]): class CodexRetryGatewayTui(App[None]):
@@ -1193,6 +1243,7 @@ def print_once(snapshot: dict[str, Any], filter_text: str = "") -> None:
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv) args = build_parser().parse_args(argv)
args.api_url = gateway_admin_url(args.api_url)
status_url = str(args.status_url or "").strip() or default_status_url() status_url = str(args.status_url or "").strip() or default_status_url()
if args.save_config or args.install: if args.save_config or args.install:
config_path = write_api_url_config(args.api_url) config_path = write_api_url_config(args.api_url)
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "codex-retry-gateway-tui" name = "codex-retry-gateway-tui"
version = "0.1.0" version = "0.1.1"
description = "Terminal UI for codex-retry-gateway monitoring and control" description = "Terminal UI for codex-retry-gateway monitoring and control"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+42
View File
@@ -1,9 +1,13 @@
from __future__ import annotations from __future__ import annotations
import importlib.util import importlib.util
import json
import os
import sys import sys
from pathlib import Path from pathlib import Path
import tempfile
import unittest import unittest
from unittest import mock
def load_module(): def load_module():
@@ -56,6 +60,10 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
def test_gateway_url_helpers_and_request_age(self) -> None: def test_gateway_url_helpers_and_request_age(self) -> None:
mod = load_module() mod = load_module()
self.assertEqual(
mod.gateway_admin_url("http://127.0.0.1:4610"),
"http://127.0.0.1:4610/__codex_retry_gateway",
)
self.assertEqual( self.assertEqual(
mod.normalize_gateway_url("http://127.0.0.1:4610/__codex_retry_gateway/api/status"), mod.normalize_gateway_url("http://127.0.0.1:4610/__codex_retry_gateway/api/status"),
"http://127.0.0.1:4610/__codex_retry_gateway", "http://127.0.0.1:4610/__codex_retry_gateway",
@@ -66,6 +74,40 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
) )
self.assertIn("updated", mod.render_request_detail({"seq": 1, "request_id": "r", "method": "POST", "path": "/responses", "status_code": 200, "upstream_status_code": 200, "upstream_attempt_count": 1, "first_response_delay_ms": 10, "duration_ms": 20, "request_body_bytes": 3, "response_bytes_received": 4, "stream_chunk_count": 1, "started_at": "2026-06-30T00:00:00Z", "finished_at": "2026-06-30T00:00:01Z", "usage_last_updated_at": "2026-06-30T00:00:01Z"})) self.assertIn("updated", mod.render_request_detail({"seq": 1, "request_id": "r", "method": "POST", "path": "/responses", "status_code": 200, "upstream_status_code": 200, "upstream_attempt_count": 1, "first_response_delay_ms": 10, "duration_ms": 20, "request_body_bytes": 3, "response_bytes_received": 4, "stream_chunk_count": 1, "started_at": "2026-06-30T00:00:00Z", "finished_at": "2026-06-30T00:00:01Z", "usage_last_updated_at": "2026-06-30T00:00:01Z"}))
def test_default_api_url_discovers_gateway_state(self) -> None:
mod = load_module()
with tempfile.TemporaryDirectory() as tmpdir:
state_path = Path(tmpdir) / "state.json"
config_path = Path(tmpdir) / "config.json"
api_url_path = Path(tmpdir) / "api-url"
state_path.write_text(json.dumps({"gateway_base_url": "http://100.115.235.115:4610"}), encoding="utf-8")
config_path.write_text("{}", encoding="utf-8")
mod.DEFAULT_GATEWAY_STATE_FILE = str(state_path)
mod.DEFAULT_GATEWAY_JSON_CONFIG_FILE = str(config_path)
with mock.patch.dict(os.environ, {"CODEX_RETRY_GATEWAY_TUI_API_URL_FILE": str(api_url_path)}, clear=False):
self.assertEqual(
mod.default_api_url(),
"http://100.115.235.115:4610/__codex_retry_gateway",
)
def test_default_api_url_discovers_gateway_config_when_state_missing(self) -> None:
mod = load_module()
with tempfile.TemporaryDirectory() as tmpdir:
state_path = Path(tmpdir) / "missing-state.json"
config_path = Path(tmpdir) / "config.json"
api_url_path = Path(tmpdir) / "api-url"
config_path.write_text(
json.dumps({"listen_host": "0.0.0.0", "listen_port": 4610}),
encoding="utf-8",
)
mod.DEFAULT_GATEWAY_STATE_FILE = str(state_path)
mod.DEFAULT_GATEWAY_JSON_CONFIG_FILE = str(config_path)
with mock.patch.dict(os.environ, {"CODEX_RETRY_GATEWAY_TUI_API_URL_FILE": str(api_url_path)}, clear=False):
self.assertEqual(
mod.default_api_url(),
"http://127.0.0.1:4610/__codex_retry_gateway",
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()