fix: auto-discover local gateway url
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user