#!/usr/bin/env python3 """Textual TUI for codex-retry-gateway monitoring and control.""" from __future__ import annotations import argparse import datetime as dt from concurrent.futures import ThreadPoolExecutor import hashlib import importlib.metadata import json import os import re import shlex import shutil import subprocess import sys import tempfile import tomllib import urllib.error import urllib.parse import urllib.request from pathlib import Path from typing import Any APP_NAME = "codex-retry-gateway-tui" 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 DEFAULT_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/api-url" DEFAULT_STATUS_CONFIG_FILE = "~/.config/codex-retry-gateway-tui/status-url" DEFAULT_ACCESS_KEY_FILE = "~/.config/codex-retry-gateway-tui/access-key" DEFAULT_REQUEST_TABLE_PREFERENCES_FILE = "~/.config/codex-retry-gateway-tui/request-table-columns.txt" LEGACY_REQUEST_TABLE_PREFERENCES_FILE = "~/.config/codex-retry-gateway-tui/request-table.json" 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_REFRESH_SECONDS = 10 DEFAULT_TIMEOUT_SECONDS = 5 DEFAULT_VERSION_CHECK_TIMEOUT_SECONDS = 2 DEFAULT_PROFILE_REASONING_EQUALS = [516, 1034, 1552] DEFAULT_PROFILE_RETRYABLE_STATUS_CODES = [429, 503] DEFAULT_PROFILE_RETRYABLE_ERROR_MESSAGES = [ "Selected model is at capacity. Please try a different model.", "stream disconnected before completion: Concurrency limit exceeded for account, please retry later", ] DEFAULT_PROFILE_UPSTREAM_FETCH_RETRY_ATTEMPTS = 5 DEFAULT_PROFILE_UPSTREAM_FETCH_RETRY_BACKOFF_MS = 350 DEFAULT_PROFILE_REQUEST_HISTORY_LIMIT = 0 REQUEST_STATUS_SLOW_AFTER_SECONDS = 20 REQUEST_STATUS_STALLED_AFTER_SECONDS = 120 DEFAULT_PROFILE_ENDPOINTS = [ "/responses", "/chat/completions", "/v1/responses", "/v1/chat/completions", ] REQUEST_TABLE_COLUMNS = ( {"key": "seq", "label": "Seq", "width_key": "seq"}, {"key": "req_id", "label": "Req ID", "width_key": "req_id"}, {"key": "resp_id", "label": "Resp ID", "width_key": "resp_id"}, {"key": "thread", "label": "Thread", "width_key": "thread"}, {"key": "started", "label": "Started", "width_key": "started"}, {"key": "status", "label": "Status", "width_key": "status"}, {"key": "path", "label": "Path", "width_key": "path"}, {"key": "model", "label": "Model", "width_key": "model"}, {"key": "effort", "label": "Effort", "width_key": "effort"}, {"key": "reasoning", "label": "R.Tok", "width_key": "reasoning"}, {"key": "in", "label": "In", "width_key": "token_col"}, {"key": "out", "label": "Out", "width_key": "token_col"}, {"key": "cache", "label": "Cache", "width_key": "token_col"}, {"key": "req_bytes", "label": "Req Size", "width_key": "req"}, {"key": "resp_bytes", "label": "Resp Size", "width_key": "resp"}, {"key": "chunks", "label": "Chunks", "width_key": "chunks"}, {"key": "first", "label": "First", "width_key": "first"}, {"key": "duration", "label": "Duration", "width_key": "duration"}, {"key": "updated", "label": "Updated", "width_key": "updated"}, {"key": "note", "label": "Note", "width_key": "note"}, {"key": "round", "label": "Round", "width_key": "round"}, ) REQUEST_TABLE_COLUMN_BY_KEY = {column["key"]: column for column in REQUEST_TABLE_COLUMNS} REQUEST_TABLE_COLUMN_KEYS = tuple(column["key"] for column in REQUEST_TABLE_COLUMNS) REQUEST_TABLE_COLUMN_KEY_SET = set(REQUEST_TABLE_COLUMN_KEYS) REQUEST_TABLE_CELL_INDEX = {key: index for index, key in enumerate(REQUEST_TABLE_COLUMN_KEYS)} DEFAULT_REQUEST_TABLE_VISIBLE_COLUMNS = REQUEST_TABLE_COLUMN_KEYS DEFAULT_REQUEST_TABLE_SORT_COLUMN = "seq" THREAD_COLOR_PALETTE = ( "#5fd7ff", "#87d75f", "#ffaf5f", "#d787ff", "#5fafff", "#ff87d7", "#d7af5f", "#5fd7af", "#af87ff", "#87afd7", ) THREAD_COLOR_COLUMN_KEYS = frozenset({"req_id", "resp_id", "thread"}) REQUEST_TABLE_WIDTH_PROFILES = { "compact": { "seq": 7, "req_id": 18, "resp_id": 18, "thread": 18, "started": 19, "status": 12, "path": 22, "model": 16, "effort": 8, "reasoning": 8, "token_col": 8, "req": 10, "resp": 10, "chunks": 14, "first": 12, "duration": 8, "updated": 8, "note": 22, "round": 10, }, "wide": { "seq": 7, "req_id": 24, "resp_id": 24, "thread": 36, "started": 19, "status": 14, "path": 40, "model": 24, "effort": 8, "reasoning": 8, "token_col": 8, "req": 12, "resp": 12, "chunks": 16, "first": 14, "duration": 8, "updated": 8, "note": 48, "round": 10, }, } REQUEST_TABLE_MIN_COLUMN_WIDTH = 4 REQUEST_TABLE_COLUMN_ALIASES = { "seq": ("seq",), "req_id": ("req id", "request id", "request_id"), "resp_id": ("resp id", "response id", "response_id"), "thread": ("thread", "thread id", "thread_id"), "started": ("started", "start", "started at"), "status": ("status", "code", "status code"), "path": ("path", "endpoint"), "model": ("model",), "effort": ("effort", "reasoning effort"), "reasoning": ("reasoning", "r tok", "rtok", "reasoning tokens", "reasoning_tokens"), "in": ("in", "input", "input tokens", "input_tokens"), "out": ("out", "output", "output tokens", "output_tokens"), "cache": ("cache", "cached", "cached tokens", "cached_tokens"), "req_bytes": ("req", "req size", "request size", "request bytes", "request_bytes"), "resp_bytes": ("resp", "resp size", "response size", "response bytes", "response_bytes"), "chunks": ("chunks", "stream chunks", "chunk count"), "first": ("first", "first response", "first delay"), "duration": ("duration", "elapsed"), "updated": ("updated", "last update"), "note": ("note", "retry note"), "round": ("round", "retry round"), } INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git" INSTALL_COMMAND_ARGS = [ "uv", "tool", "install", "--force", "git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git", ] def env_int(name: str, default: int, *, minimum: int = 1) -> int: try: return max(minimum, int(os.environ.get(name, default))) except Exception: return default def configured_url(env_names: tuple[str, ...], config_path: str, default: str = "") -> str: for name in env_names: value = os.environ.get(name, "").strip() if value: return value config_file = Path(config_path).expanduser() try: for line in config_file.read_text(encoding="utf-8").splitlines(): value = line.strip() if value and not value.startswith("#"): return value except OSError: pass 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: configured = configured_url( ("CODEX_RETRY_GATEWAY_TUI_API_URL",), os.environ.get("CODEX_RETRY_GATEWAY_TUI_API_URL_FILE", DEFAULT_CONFIG_FILE), "", ) 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: return Path(os.environ.get("CODEX_RETRY_GATEWAY_TUI_API_URL_FILE", DEFAULT_CONFIG_FILE)).expanduser() def access_key_file_path() -> Path: return Path(os.environ.get("CODEX_RETRY_GATEWAY_TUI_ACCESS_KEY_FILE", DEFAULT_ACCESS_KEY_FILE)).expanduser() def request_table_preferences_file_path() -> Path: return Path( os.environ.get( "CODEX_RETRY_GATEWAY_TUI_REQUEST_TABLE_PREFERENCES_FILE", DEFAULT_REQUEST_TABLE_PREFERENCES_FILE, ) ).expanduser() def legacy_request_table_preferences_file_path() -> Path: return Path(LEGACY_REQUEST_TABLE_PREFERENCES_FILE).expanduser() def default_status_url() -> str: return configured_url( ("CODEX_RETRY_GATEWAY_TUI_STATUS_URL",), os.environ.get("CODEX_RETRY_GATEWAY_TUI_STATUS_URL_FILE", DEFAULT_STATUS_CONFIG_FILE), "", ) def default_access_key() -> str: return configured_url( ("CODEX_RETRY_GATEWAY_TUI_ACCESS_KEY",), os.environ.get("CODEX_RETRY_GATEWAY_TUI_ACCESS_KEY_FILE", DEFAULT_ACCESS_KEY_FILE), "", ) def request_table_column_token(value: Any) -> str: return re.sub(r"[^a-z0-9]+", "", str(value or "").strip().lower()) def build_request_table_column_token_map() -> dict[str, str]: token_map: dict[str, str] = {} for column in REQUEST_TABLE_COLUMNS: key = column["key"] aliases = set(REQUEST_TABLE_COLUMN_ALIASES.get(key, ())) aliases.add(key) aliases.add(column["label"]) for alias in aliases: token = request_table_column_token(alias) if token: token_map[token] = key return token_map REQUEST_TABLE_COLUMN_TOKEN_MAP = build_request_table_column_token_map() def request_table_column_key(value: Any) -> str: token = request_table_column_token(value) return REQUEST_TABLE_COLUMN_TOKEN_MAP.get(token, "") def request_table_column_keys(value: Any) -> list[str]: token = request_table_column_token(value) if token == "usage": return ["in", "out", "cache"] column_key = REQUEST_TABLE_COLUMN_TOKEN_MAP.get(token, "") return [column_key] if column_key else [] def request_table_column_order(value: Any) -> list[str]: requested = value if isinstance(value, (list, tuple, set)) else [] columns = [] seen: set[str] = set() for item in requested: for column_key in request_table_column_keys(item): if not column_key or column_key in seen: continue columns.append(column_key) seen.add(column_key) for column_key in REQUEST_TABLE_COLUMN_KEYS: if column_key not in seen: columns.append(column_key) return columns or list(DEFAULT_REQUEST_TABLE_VISIBLE_COLUMNS) def request_table_hidden_columns(value: Any, columns: list[str]) -> list[str]: requested = value if isinstance(value, (list, tuple, set)) else [] allowed = set(columns) hidden = [] seen: set[str] = set() for item in requested: for column_key in request_table_column_keys(item): if not column_key or column_key not in allowed or column_key in seen: continue hidden.append(column_key) seen.add(column_key) return hidden def request_table_column_width_value(value: Any) -> int | None: try: width = int(str(value or "").strip()) except Exception: return None if width <= 0: return None return max(REQUEST_TABLE_MIN_COLUMN_WIDTH, width) def request_table_width_overrides(value: Any) -> dict[str, int]: requested = value if isinstance(value, dict) else {} overrides: dict[str, int] = {} for raw_key, raw_width in requested.items(): width = request_table_column_width_value(raw_width) if width is None: continue for column_key in request_table_column_keys(raw_key): if column_key: overrides[column_key] = width return overrides def normalize_request_table_sort_column(value: Any) -> str: column_key = str(value or "").strip() if column_key in REQUEST_TABLE_COLUMN_KEY_SET: return column_key return DEFAULT_REQUEST_TABLE_SORT_COLUMN def normalize_request_table_preferences(data: Any) -> dict[str, Any]: source = data if isinstance(data, dict) else {} if isinstance(source.get("visible_columns"), (list, tuple, set)): legacy_visible = [] seen: set[str] = set() for item in source.get("visible_columns") or []: for column_key in request_table_column_keys(item): if not column_key or column_key in seen: continue legacy_visible.append(column_key) seen.add(column_key) columns = request_table_column_order(legacy_visible) hidden_columns = [column_key for column_key in columns if column_key not in seen] else: columns = request_table_column_order(source.get("columns")) hidden_columns = request_table_hidden_columns(source.get("hidden_columns"), columns) if len(hidden_columns) >= len(columns) and columns: hidden_columns = [column_key for column_key in hidden_columns if column_key != columns[0]] width_overrides = request_table_width_overrides(source.get("widths")) return { "columns": columns, "hidden_columns": hidden_columns, "widths": {column_key: width for column_key, width in width_overrides.items() if column_key in columns}, } def request_table_visible_columns(preferences: dict[str, Any]) -> list[str]: normalized = normalize_request_table_preferences(preferences) hidden = set(normalized["hidden_columns"]) visible = [column_key for column_key in normalized["columns"] if column_key not in hidden] if visible: return visible return [normalized["columns"][0]] def request_table_preferences_from_text(text: str) -> dict[str, Any]: columns = [] hidden_columns = [] width_overrides: dict[str, int] = {} seen: set[str] = set() for line_number, raw_line in enumerate(text.splitlines(), start=1): stripped = raw_line.strip() if not stripped: continue hidden = False token = stripped if token.startswith("#"): hidden = True token = token[1:].strip() if not token: continue width = None width_match = re.match(r"^(.*?)(?:\s+(\d+))?$", token) if width_match: token = (width_match.group(1) or "").strip() width = request_table_column_width_value(width_match.group(2)) column_keys = request_table_column_keys(token) if not column_keys: if hidden: continue raise ValueError(f"line {line_number}: unknown request column {token!r}") for column_key in column_keys: if column_key in seen: continue seen.add(column_key) columns.append(column_key) if hidden: hidden_columns.append(column_key) if width is not None: width_overrides[column_key] = width if not columns: raise ValueError("no request columns found in editor document") missing = [column_key for column_key in REQUEST_TABLE_COLUMN_KEYS if column_key not in seen] return normalize_request_table_preferences( { "columns": columns + missing, "hidden_columns": hidden_columns + missing, "widths": width_overrides, } ) def request_table_preferences_document(preferences: dict[str, Any]) -> str: normalized = normalize_request_table_preferences(preferences) hidden = set(normalized["hidden_columns"]) widths = normalized["widths"] lines = [ "# Request table columns.", "# One column key per line.", "# Prefix with # to hide a column.", "# Optional: append an integer width, e.g. `first 14` or `in 10`.", "# Move lines up or down to change display order.", "# Common keys: in, out, cache, req_id, resp_id, req_bytes, resp_bytes, round.", "", ] for column_key in normalized["columns"]: prefix = "# " if column_key in hidden else "" width_suffix = f" {widths[column_key]}" if column_key in widths else "" lines.append(f"{prefix}{column_key}{width_suffix}") return "\n".join(lines).rstrip() + "\n" def load_request_table_preferences() -> dict[str, Any]: configured_path = request_table_preferences_file_path() configured_env = os.environ.get("CODEX_RETRY_GATEWAY_TUI_REQUEST_TABLE_PREFERENCES_FILE", "").strip() candidate_paths = [configured_path] if not configured_env: candidate_paths.append(legacy_request_table_preferences_file_path()) for path in candidate_paths: try: text = path.read_text(encoding="utf-8") except OSError: continue stripped = text.strip() if not stripped: continue try: if stripped.startswith("{"): return normalize_request_table_preferences(json.loads(text)) return request_table_preferences_from_text(text) except Exception: continue return normalize_request_table_preferences({}) def write_request_table_preferences(preferences: dict[str, Any]) -> Path: path = request_table_preferences_file_path() path.parent.mkdir(parents=True, exist_ok=True) try: path.parent.chmod(0o700) except OSError: pass normalized = normalize_request_table_preferences(preferences) path.write_text(request_table_preferences_document(normalized), encoding="utf-8") try: path.chmod(0o600) except OSError: pass return path def normalize_gateway_url(api_url: str) -> str: value = str(api_url or "").strip() if not value: return "" parsed = urllib.parse.urlparse(value) path = parsed.path.rstrip("/") for suffix in ( "/api/status", "/api/logs", "/api/requests", "/api/profiles", "/api/image-profiles", "/api/config", "/api/restore", ): if path.endswith(suffix): path = path[: -len(suffix)] break if path.endswith("/api"): path = path[:-4] normalized = parsed._replace(path=path, params="", query="", fragment="") return urllib.parse.urlunparse(normalized).rstrip("/") def gateway_status_url(gateway_url: str) -> str: return f"{normalize_gateway_url(gateway_url).rstrip('/')}/api/status" def build_api_url(gateway_url: str, suffix: str, params: dict[str, Any] | None = None) -> str: parsed = urllib.parse.urlparse(action_url(gateway_url, suffix)) query_items: list[tuple[str, str]] = [] if parsed.query: query_items.extend(urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)) for key, value in (params or {}).items(): if value is None: continue if isinstance(value, str) and not value.strip(): continue query_items.append((key, str(value))) if query_items: parsed = parsed._replace(query=urllib.parse.urlencode(query_items, doseq=True)) return urllib.parse.urlunparse(parsed) def write_api_url_config(api_url: str) -> Path: value = gateway_admin_url(api_url) if not value: raise ValueError("api url is empty") path = config_file_path() path.parent.mkdir(parents=True, exist_ok=True) try: path.parent.chmod(0o700) except OSError: pass path.write_text(value + "\n", encoding="utf-8") try: path.chmod(0o600) except OSError: pass return path def write_access_key_config(access_key: str) -> Path: path = access_key_file_path() path.parent.mkdir(parents=True, exist_ok=True) try: path.parent.chmod(0o700) except OSError: pass path.write_text(str(access_key or "").strip() + "\n", encoding="utf-8") try: path.chmod(0o600) except OSError: pass return path def run_install_command() -> int: try: return subprocess.run(INSTALL_COMMAND_ARGS, check=False).returncode except FileNotFoundError: print("uv command not found; install uv first, then run:", INSTALL_COMMAND, file=sys.stderr) return 127 def current_version() -> str: try: return importlib.metadata.version(APP_NAME) except importlib.metadata.PackageNotFoundError: return FALLBACK_VERSION except Exception: return FALLBACK_VERSION def parse_version(value: Any) -> tuple[int, ...]: parts = [] for part in re.split(r"[^0-9]+", str(value or "")): if part: parts.append(int(part)) return tuple(parts) def version_is_newer(latest: str, current: str) -> bool: latest_parts = parse_version(latest) current_parts = parse_version(current) width = max(len(latest_parts), len(current_parts), 1) return latest_parts + (0,) * (width - len(latest_parts)) > current_parts + (0,) * (width - len(current_parts)) def latest_version_from_text(text: str) -> str: match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text) return "" if not match else match.group(1).strip() def fetch_latest_version(url: str, timeout: int) -> str: req = urllib.request.Request(url, headers={"Accept": "text/plain"}) with urllib.request.urlopen(req, timeout=timeout) as response: return latest_version_from_text(response.read(65536).decode("utf-8", errors="replace")) def version_update_message(latest: str, current: str | None = None) -> str: current = current or current_version() if not latest or not version_is_newer(latest, current): return "" return f"update available: {APP_NAME} {current} -> {latest}; run `{INSTALL_COMMAND}`" def check_version_update(url: str, timeout: int, *, disabled: bool = False) -> str: if disabled or os.environ.get("CODEX_RETRY_GATEWAY_TUI_NO_VERSION_CHECK", "").strip().lower() in {"1", "true", "yes", "on"}: return "" try: return version_update_message(fetch_latest_version(url, timeout)) except Exception: return "" def as_float(value: Any) -> float: try: if value is None or str(value).strip() == "": return 0.0 return float(value) except Exception: return 0.0 def as_int(value: Any) -> int: try: if value is None or str(value).strip() == "": return 0 return int(float(value)) except Exception: return 0 def short_text(value: Any, length: int = 48) -> str: text = str(value or "").strip() if not text: return "-" return text if len(text) <= length else f"{text[: length - 3]}..." def short_time(value: Any) -> str: text = str(value or "").strip() if not text: return "-" return text.replace("T", " ")[:19] def format_ms(value: Any) -> str: number = as_int(value) return "-" if number <= 0 else f"{number}ms" def format_duration_ms_as_seconds(value: Any) -> str: number = as_float(value) if number <= 0: return "-" return format_elapsed_seconds(number / 1000.0) def format_bytes(value: Any) -> str: if value is None or str(value).strip() == "": return "-" number = as_int(value) if number < 0: return "-" if number == 0: return "0B" if number >= 1_000_000: return f"{number / 1_000_000:.1f}MB".rstrip("0").rstrip(".") if number >= 1_000: return f"{number / 1_000:.1f}KB".rstrip("0").rstrip(".") return f"{number}B" def format_percent(value: Any) -> str: if value is None or str(value).strip() == "": return "-" percent = as_float(value) rounded = round(percent, 1) if rounded.is_integer(): return f"{int(rounded)}%" return f"{rounded}%" def cached_ratio(input_tokens: Any, cached_tokens: Any) -> float | None: total = as_float(input_tokens) if total <= 0: return None cached = max(0.0, as_float(cached_tokens)) return max(0.0, min(1.0, cached / total)) def effective_input_tokens(input_tokens: Any, cached_tokens: Any) -> int | None: total = parse_int_value(input_tokens) if total is None: return None cached = max(0, as_int(cached_tokens)) return max(0, total - cached) def request_id_text(row: dict[str, Any]) -> str: return str(row.get("request_id") or "").strip() or "-" def response_id_text(row: dict[str, Any]) -> str: return str(row.get("response_id") or "").strip() or "-" def request_usage_summary(row: dict[str, Any]) -> str: effective_in = effective_input_tokens(row.get("input_tokens"), row.get("cached_tokens")) output_tokens = row.get("output_tokens") cached_tokens = row.get("cached_tokens") ratio = cached_ratio(row.get("input_tokens"), cached_tokens) in_text = format_count(effective_in) if effective_in is not None else "-" out_text = format_count(output_tokens) if output_tokens is not None else "-" cached_text = format_count(cached_tokens) if cached_tokens is not None else "-" if ratio is not None: cached_text = f"{cached_text} ({format_percent(ratio * 100)})" return f"in {in_text} | out {out_text} | cached {cached_text}" def request_input_tokens_text(row: dict[str, Any]) -> str: effective_in = effective_input_tokens(row.get("input_tokens"), row.get("cached_tokens")) return format_count(effective_in) if effective_in is not None else "-" def request_output_tokens_text(row: dict[str, Any]) -> str: output_tokens = row.get("output_tokens") return format_count(output_tokens) if output_tokens is not None else "-" def request_cached_tokens_text(row: dict[str, Any]) -> str: cached_tokens = row.get("cached_tokens") return format_count(cached_tokens) if cached_tokens is not None else "-" def format_count(value: Any) -> str: number = as_int(value) if abs(number) >= 1_000_000: return f"{number / 1_000_000:.1f}M".rstrip("0").rstrip(".") if abs(number) >= 1_000: return f"{number / 1_000:.1f}K".rstrip("0").rstrip(".") return str(number) def parse_datetime(value: Any) -> dt.datetime | None: text = str(value or "").strip() if not text: return None try: normalized = text[:-1] + "+00:00" if text.endswith("Z") else text return dt.datetime.fromisoformat(normalized) except Exception: return None def format_elapsed_seconds(value: Any) -> str: number = as_float(value) if number <= 0: return "-" if number >= 100: return f"{number:.0f}s" return f"{number:.1f}s" def elapsed_between(started_at: Any, finished_at: Any) -> str: start = parse_datetime(started_at) finish = parse_datetime(finished_at) if not start or not finish: return "-" seconds = max(0.0, (finish - start).total_seconds()) return format_elapsed_seconds(seconds) def age_since(value: Any) -> str: parsed = parse_datetime(value) if not parsed: return "-" now = dt.datetime.now(parsed.tzinfo) if parsed.tzinfo else dt.datetime.now() seconds = max(0.0, (now - parsed).total_seconds()) return format_elapsed_seconds(seconds) def request_updated_at(row: dict[str, Any]) -> Any: for key in ("last_activity_at", "usage_last_updated_at", "finished_at"): value = row.get(key) if parse_datetime(value): return value return None def request_updated_elapsed(row: dict[str, Any]) -> str: return elapsed_between(row.get("started_at"), request_updated_at(row)) def request_chunk_progress(row: dict[str, Any]) -> str: if not row.get("response_stream"): return "-" return f"{row.get('stream_chunk_count') or 0} / {format_bytes(row.get('response_bytes_received'))}" def request_effort_text(row: dict[str, Any]) -> str: return str(row.get("reasoning_effort") or "").strip() or "-" def request_model_text(row: dict[str, Any]) -> str: return str(row.get("model") or row.get("requested_model") or row.get("forwarded_model") or "").strip() def normalize_request_retry_firsts(value: Any) -> list[dict[str, Any]]: if not isinstance(value, (list, tuple)): return [] firsts = [] for item in value: if not isinstance(item, dict): continue firsts.append( { "round": parse_int_value(item.get("round")), "slot": parse_int_value(item.get("slot")), "first_response_at": item.get("first_response_at"), "first_delay_ms": item.get("first_response_delay_ms", item.get("first_delay_ms", item.get("first_ms"))), "outcome": str(item.get("outcome") or "").strip(), "status_code": item.get("status_code", item.get("upstream_status_code", item.get("status"))), "reasoning_effort": str(item.get("reasoning_effort") or "").strip(), "reasoning_tokens": item.get("reasoning_tokens"), "reasoning": str(item.get("reasoning") or item.get("reason") or item.get("error") or "").strip(), } ) return firsts def request_retry_first_label(first: dict[str, Any], fallback_round: Any = None) -> str: round_number = parse_int_value(first.get("round")) if round_number is None: round_number = parse_int_value(fallback_round) slot = parse_int_value(first.get("slot")) if round_number is not None: if slot is not None and slot > 1: return f"{round_number}-{slot}" return str(round_number) if slot is not None: return str(slot) return "?" def request_retry_round_text(row: dict[str, Any]) -> str: round_number = parse_int_value(row.get("reasoning_retry_current_round")) width = parse_int_value(row.get("reasoning_retry_current_width")) if round_number is None and width is None: return "-" if round_number is None: return f"-({max(0, width or 0)})" if width is None: return str(round_number) return f"{round_number}({max(0, width)})" def request_retry_first_summary(first: dict[str, Any], fallback_round: Any = None) -> str: bits = [] round_label = request_retry_first_label(first, fallback_round) if round_label != "?": bits.append(f"round {round_label}") slot = parse_int_value(first.get("slot")) if slot is not None: bits.append(f"slot {slot}") first_delay = format_duration_ms_as_seconds(first.get("first_delay_ms")) if first_delay != "-": bits.append(f"first {first_delay}") outcome = str(first.get("outcome") or "").strip() status_code = first.get("status_code") status_bits = [bit for bit in (outcome, str(status_code).strip() if status_code not in (None, "") else "") if bit] if status_bits: bits.append(f"status {'/'.join(status_bits)}") reasoning_bits = [] if first.get("reasoning_effort"): reasoning_bits.append(str(first["reasoning_effort"])) reasoning_tokens = parse_int_value(first.get("reasoning_tokens")) if reasoning_tokens is not None: reasoning_bits.append(format_count(reasoning_tokens)) reasoning = str(first.get("reasoning") or "").strip() if reasoning_bits or reasoning: detail = "/".join(reasoning_bits) if reasoning: detail = f"{detail} {short_text(reasoning, 40)}".strip() bits.append(f"reasoning {detail}".strip()) return " ".join(bits) def request_retry_wave_summary(row: dict[str, Any]) -> str: summaries = [] fallback_round = row.get("reasoning_retry_current_round") for first in row.get("reasoning_retry_current_firsts") or []: if not isinstance(first, dict): continue summary = request_retry_first_summary(first, fallback_round) if summary: summaries.append(summary) return "; ".join(summaries) def request_retry_firsts_compact_text(row: dict[str, Any]) -> str: parts = [] for first in row.get("reasoning_retry_current_firsts") or []: if not isinstance(first, dict): continue delay = format_duration_ms_as_seconds(first.get("first_delay_ms")) parts.append(delay if delay != "-" else "?") return " ".join(parts) def request_first_text(row: dict[str, Any]) -> str: retry_firsts = request_retry_firsts_compact_text(row) if retry_firsts: return retry_firsts return format_duration_ms_as_seconds(row["first_response_delay_ms"]) def request_retry_note(row: dict[str, Any]) -> str: attempts = as_int(row.get("upstream_attempt_count")) if attempts <= 1: return "" return str(row.get("error") or "").strip() def request_reasoning_tokens_text(row: dict[str, Any]) -> str: return format_count(row["reasoning_tokens"]) if row["reasoning_tokens"] is not None else "-" def request_row_cells(row: dict[str, Any], text_limits: dict[str, int] | None = None) -> tuple[Any, ...]: limits = text_limits or {} note_width = max(4, limits.get("note", 22)) retry_note = request_retry_note(row) return ( str(row["seq"]), short_text(request_id_text(row), limits.get("req_id", 18)) or "-", short_text(response_id_text(row), limits.get("resp_id", 18)) or "-", short_text(row["thread_id"], limits.get("thread", 18)) or "-", short_time(row["started_at"]), request_status_label(row), short_text(row["path"], limits.get("path", 22)), short_text(request_model_text(row), limits.get("model", 16)), short_text(request_effort_text(row), limits.get("effort", 10)), request_reasoning_tokens_text(row), request_input_tokens_text(row), request_output_tokens_text(row), request_cached_tokens_text(row), format_bytes(row.get("request_body_bytes")), format_bytes(row.get("response_bytes_received")), request_chunk_progress(row), short_text(request_first_text(row), limits.get("first", 10)), format_duration_ms_as_seconds(row["duration_ms"]), request_updated_elapsed(row), short_text(retry_note, note_width), short_text(request_retry_round_text(row), limits.get("round", 10)), ) def request_thread_color(thread_id: Any) -> str | None: text = str(thread_id or "").strip() if not text: return None digest = hashlib.blake2b(text.encode("utf-8"), digest_size=4).digest() color_index = int.from_bytes(digest[:2], "big") % len(THREAD_COLOR_PALETTE) return THREAD_COLOR_PALETTE[color_index] def request_visible_cells( row: dict[str, Any], cells: tuple[Any, ...], visible_column_keys: list[str], ) -> list[Any]: visible_cells = [cells[REQUEST_TABLE_CELL_INDEX[column_key]] for column_key in visible_column_keys] try: from rich.text import Text except ImportError: return visible_cells thread_color = request_thread_color(row.get("thread_id")) styled_cells: list[Any] = [] for column_key, cell in zip(visible_column_keys, visible_cells): if column_key == "status": styled_cells.append(Text(str(cell), style=request_status_style(row))) elif thread_color and column_key in THREAD_COLOR_COLUMN_KEYS: styled_cells.append(Text(str(cell), style=f"bold {thread_color}")) else: styled_cells.append(cell) return styled_cells def request_table_column_label(column_key: str) -> str: column = REQUEST_TABLE_COLUMN_BY_KEY.get(column_key) return str(column["label"]) if column else column_key def request_sort_value(row: dict[str, Any], column_key: str) -> Any: if column_key == "seq": return parse_int_value(row.get("seq")) if column_key == "req_id": text = request_id_text(row) return None if text == "-" else text.lower() if column_key == "resp_id": text = response_id_text(row) return None if text == "-" else text.lower() if column_key == "thread": text = str(row.get("thread_id") or "").strip() return text.lower() if text else None if column_key == "started": started = parse_datetime(row.get("started_at")) return started.timestamp() if started else None if column_key == "status": return parse_int_value(row.get("status_code")) if column_key == "path": text = str(row.get("path") or "").strip() return text.lower() if text else None if column_key == "model": text = request_model_text(row) return text.lower() if text else None if column_key == "effort": text = str(row.get("reasoning_effort") or "").strip() return text.lower() if text else None if column_key == "reasoning": return parse_int_value(row.get("reasoning_tokens")) if column_key == "usage": total_tokens = parse_int_value(row.get("total_tokens")) if total_tokens is not None: return total_tokens input_tokens = parse_int_value(row.get("input_tokens")) or 0 output_tokens = parse_int_value(row.get("output_tokens")) or 0 cached_tokens = parse_int_value(row.get("cached_tokens")) or 0 combined = input_tokens + output_tokens + cached_tokens return combined if combined else None if column_key == "req_bytes": return parse_int_value(row.get("request_body_bytes")) if column_key == "resp_bytes": return parse_int_value(row.get("response_bytes_received")) if column_key == "chunks": return parse_int_value(row.get("stream_chunk_count")) if column_key == "first": return parse_int_value(row.get("first_response_delay_ms")) if column_key == "duration": return parse_int_value(row.get("duration_ms")) if column_key == "updated": updated = parse_datetime(request_updated_at(row)) return updated.timestamp() if updated else None if column_key == "note": text = request_retry_note(row) return text.lower() if text else None if column_key == "round": round_number = parse_int_value(row.get("reasoning_retry_current_round")) width = parse_int_value(row.get("reasoning_retry_current_width")) if round_number is None and width is None: return None return (round_number or 0, width or 0) return None def sort_request_rows( rows: list[dict[str, Any]], sort_column: str = DEFAULT_REQUEST_TABLE_SORT_COLUMN, *, reverse: bool = True, ) -> list[dict[str, Any]]: column_key = normalize_request_table_sort_column(sort_column) known: list[tuple[Any, dict[str, Any]]] = [] missing: list[dict[str, Any]] = [] for row in rows: value = request_sort_value(row, column_key) if value is None or value == "": missing.append(row) continue known.append((value, row)) known.sort(key=lambda item: item[0], reverse=reverse) return [row for _, row in known] + missing def status_kind(value: Any) -> str: text = str(value or "").strip().lower() if text in {"ok", "operational", "success"}: return "ok" if text in {"failed", "error", "failure"}: return "failed" return "unknown" def request_headers(access_key: str = "", *, accept: str = "application/json") -> dict[str, str]: headers = {"Accept": accept} if str(access_key or "").strip(): headers["x-codex-retry-gateway-key"] = str(access_key).strip() return headers def fetch_payload(api_url: str, timeout: int, access_key: str = "") -> dict[str, Any]: req = urllib.request.Request(api_url, headers=request_headers(access_key)) with urllib.request.urlopen(req, timeout=timeout) as response: data = json.loads(response.read().decode("utf-8")) if not isinstance(data, dict): raise RuntimeError("API did not return a JSON object") return data 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) def text_tokens(value: Any) -> set[str]: tokens = set() for token in "".join(ch.lower() if ch.isalnum() else " " for ch in str(value or "")).split(): if token and not token.isdigit(): tokens.add(token) return tokens def format_bool(value: Any) -> str: return "yes" if bool(value) else "no" def parse_bool_value(value: Any, default: bool = False) -> bool: if isinstance(value, bool): return value if value is None: return default text = str(value).strip().lower() if text in {"1", "true", "yes", "on"}: return True if text in {"0", "false", "no", "off", ""}: return False return bool(value) def parse_int_value(value: Any) -> int | None: if value is None or str(value).strip() == "": return None try: return int(str(value).strip()) except Exception: try: return int(float(str(value).strip())) except Exception: return None def normalize_editor_integer_list(value: Any) -> list[int]: if isinstance(value, (list, tuple, set)): raw_values = list(value) else: raw_values = re.split(r"[\s,]+", str(value or "")) numbers: list[int] = [] for item in raw_values: parsed = parse_int_value(item) if parsed is not None: numbers.append(parsed) return numbers def normalize_editor_string_list(value: Any) -> list[str]: if isinstance(value, (list, tuple, set)): raw_values = list(value) else: raw_values = re.split(r"[\r\n,]+", str(value or "")) values = [] for item in raw_values: text = str(item or "").strip() if text: values.append(text) return values def normalize_editor_phrase_list(value: Any) -> list[str]: if isinstance(value, (list, tuple, set)): raw_values = list(value) else: raw_values = str(value or "").splitlines() values = [] for item in raw_values: text = str(item or "").strip() if text: values.append(text) return values def toml_string(value: Any) -> str: return json.dumps(str(value or ""), ensure_ascii=False) def toml_bool(value: Any) -> str: return "true" if parse_bool_value(value) else "false" def toml_int_list(values: Any) -> str: numbers = normalize_editor_integer_list(values) return "[" + ", ".join(str(value) for value in numbers) + "]" def toml_string_list(values: Any) -> str: items = [toml_string(value) for value in normalize_editor_string_list(values)] return "[" + ", ".join(items) + "]" def toml_phrase_list(values: Any) -> str: items = [toml_string(value) for value in normalize_editor_phrase_list(values)] return "[" + ", ".join(items) + "]" def summary_line(payload: dict[str, Any]) -> str: config = payload.get("config") if isinstance(payload.get("config"), dict) else {} state = payload.get("state") if isinstance(payload.get("state"), dict) else {} metrics = payload.get("metrics") if isinstance(payload.get("metrics"), dict) else {} listen = payload.get("listen") or "-" upstream = config.get("upstream_base_url") or "-" active = config.get("profile_name") or state.get("profile_name") or "-" image_active = config.get("image_profile_name") or state.get("image_profile_name") or "-" request_total = format_count(metrics.get("total_proxy_request_count")) inspected = format_count(metrics.get("inspected_response_count")) matched = format_count(metrics.get("matched_response_count")) reasoning_516 = format_count(metrics.get("reasoning_516_count")) return f"{listen} | text {active} | image {image_active} | upstream {upstream} | req {request_total} | inspected {inspected} | matched {matched} | 516 {reasoning_516}" def state_summary(payload: dict[str, Any]) -> str: state = payload.get("state") if isinstance(payload.get("state"), dict) else {} paths = payload.get("paths") if isinstance(payload.get("paths"), dict) else {} bits = [ f"state {state.get('state_root') or paths.get('state_root') or '-'}", f"config {paths.get('config_path') or '-'}", f"requests {paths.get('requests_path') or '-'}", f"profiles {paths.get('profiles_dir') or '-'}", f"image profiles {paths.get('image_profiles_dir') or '-'}", ] if state.get("gateway_base_url"): bits.append(f"gateway {state.get('gateway_base_url')}") if state.get("codex_current_base_url"): bits.append(f"codex {state.get('codex_current_base_url')}") return " | ".join(bits) def status_summary(status_payload: dict[str, Any], status_error: str = "") -> str: if status_error: return f"status error: {status_error}" config = status_payload.get("config") if isinstance(status_payload.get("config"), dict) else {} state = status_payload.get("state") if isinstance(status_payload.get("state"), dict) else {} metrics = status_payload.get("metrics") if isinstance(status_payload.get("metrics"), dict) else {} listen = status_payload.get("listen") or "-" return ( f"live {listen} | text {config.get('profile_name') or state.get('profile_name') or '-'} | image {config.get('image_profile_name') or state.get('image_profile_name') or '-'} | " f"516 {metrics.get('reasoning_516_count') or 0} | latest seq {metrics.get('total_proxy_request_count') or 0}" ) def request_match_score(row: dict[str, Any], needle: str) -> bool: if not needle: return True haystack = " ".join( str(row.get(key) or "") for key in ( "request_id", "response_id", "thread_id", "profile_name", "method", "path", "model", "requested_model", "forwarded_model", "reasoning_effort", "lifecycle_state", "discard_reason", "error", "upstream_origin", "upstream_path", "status_code", "upstream_status_code", "reasoning_tokens", "input_tokens", "output_tokens", "total_tokens", "cached_tokens", "reasoning_retry_current_round", "reasoning_retry_current_width", ) ).lower() current_firsts = json.dumps(row.get("reasoning_retry_current_firsts") or [], ensure_ascii=False).lower() haystack = f"{haystack} {current_firsts}" return needle in haystack def request_retry_first_received_at(row: dict[str, Any]) -> Any: latest_value = None latest_at = None for first in row.get("reasoning_retry_current_firsts") or []: if not isinstance(first, dict): continue value = first.get("first_response_at") parsed = parse_datetime(value) if parsed and (latest_at is None or parsed > latest_at): latest_value = value latest_at = parsed return latest_value def request_has_received_first(row: dict[str, Any]) -> bool: if parse_datetime(row.get("first_response_at")): return True for first in row.get("reasoning_retry_current_firsts") or []: if not isinstance(first, dict): continue if parse_datetime(first.get("first_response_at")) or parse_int_value(first.get("first_delay_ms")) is not None: return True return False def request_lifecycle_label(row: dict[str, Any]) -> str: state = str(row.get("lifecycle_state") or "").strip().lower() if request_is_discarded(row): return "discarded" if state in {"finish", "finished", "complete", "completed"} or parse_datetime(row.get("finished_at")): return "finished" if state in {"receive_first", "streaming"} or request_has_received_first(row): return "streaming" return "waiting" def request_is_discarded(row: dict[str, Any]) -> bool: state = str(row.get("lifecycle_state") or "").strip().lower() if bool(row.get("discarded")) or state in {"discarded", "abandoned", "cancelled", "canceled"}: return True error = str(row.get("error") or "").strip().lower() return any( marker in error for marker in ( "client disconnected", "client closed", "cancelled by client", "canceled by client", "request aborted", ) ) def request_activity_at(row: dict[str, Any]) -> Any: candidates = [row.get("last_activity_at"), row.get("first_response_at"), request_retry_first_received_at(row), row.get("started_at")] parsed_candidates = [(parse_datetime(value), value) for value in candidates] valid_candidates = [(parsed, value) for parsed, value in parsed_candidates if parsed] if not valid_candidates: return None return max(valid_candidates, key=lambda item: item[0])[1] def request_activity_age_seconds(row: dict[str, Any], now: dt.datetime | None = None) -> float | None: activity_at = parse_datetime(request_activity_at(row)) if not activity_at: return None current = now or dt.datetime.now(activity_at.tzinfo) if current.tzinfo is None and activity_at.tzinfo is not None: current = current.replace(tzinfo=activity_at.tzinfo) return max(0.0, (current - activity_at).total_seconds()) def request_status_label(row: dict[str, Any], now: dt.datetime | None = None) -> str: if request_is_discarded(row): return "discarded" status_code = parse_int_value(row.get("status_code")) if status_code is not None: return f"HTTP {status_code}" lifecycle = request_lifecycle_label(row) if lifecycle == "finished": return "finished" age_seconds = request_activity_age_seconds(row, now) if age_seconds is not None and age_seconds >= REQUEST_STATUS_STALLED_AFTER_SECONDS: return f"stalled {format_elapsed_seconds(age_seconds)}" if age_seconds is not None and age_seconds >= REQUEST_STATUS_SLOW_AFTER_SECONDS: return f"{lifecycle} {format_elapsed_seconds(age_seconds)}" return lifecycle def request_status_style(row: dict[str, Any], now: dt.datetime | None = None) -> str: label = request_status_label(row, now).lower() if label.startswith(("discarded", "stalled")): return "bold #ff5f5f" if label.startswith("waiting"): return "bold #ffaf5f" if label.startswith("streaming"): return "bold #5fd7ff" if label.startswith("http "): status_code = parse_int_value(row.get("status_code")) or 0 if 200 <= status_code < 400: return "bold #87d75f" if 400 <= status_code < 500: return "bold #ffaf5f" return "bold #ff5f5f" if row.get("error"): return "bold #ff5f5f" return "#b0b0b0" def status_symbol(entry: dict[str, Any]) -> str: """Compatibility helper for callers that previously consumed a status marker.""" return request_status_label(entry) def normalize_request_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]: needle = filter_text.strip().lower() rows = [] for entry in payload.get("entries") or []: if not isinstance(entry, dict): continue if needle and not request_match_score(entry, needle): continue usage = entry.get("usage") if isinstance(entry.get("usage"), dict) else {} rows.append( { "seq": as_int(entry.get("seq")), "request_id": str(entry.get("request_id") or ""), "response_id": str(entry.get("response_id") or ""), "thread_id": str(entry.get("thread_id") or ""), "profile_name": str(entry.get("profile_name") or ""), "method": str(entry.get("method") or ""), "path": str(entry.get("path") or ""), "model": str(entry.get("model") or ""), "requested_model": str(entry.get("requested_model") or ""), "forwarded_model": str(entry.get("forwarded_model") or ""), "reasoning_effort": str(entry.get("reasoning_effort") or ""), "reasoning_summary": str(entry.get("reasoning_summary") or ""), "request_stream": bool(entry.get("request_stream")), "response_stream": bool(entry.get("response_stream")), "lifecycle_state": str(entry.get("lifecycle_state") or ""), "discarded": bool(entry.get("discarded")), "discard_reason": str(entry.get("discard_reason") or ""), "matched": bool(entry.get("matched")), "inspected": bool(entry.get("inspected")), "status_code": entry.get("status_code"), "upstream_status_code": entry.get("upstream_status_code"), "reasoning_tokens": entry.get("reasoning_tokens", usage.get("reasoning_tokens")), "input_tokens": entry.get("input_tokens", usage.get("input_tokens")), "output_tokens": entry.get("output_tokens", usage.get("output_tokens")), "total_tokens": entry.get("total_tokens", usage.get("total_tokens")), "cached_tokens": entry.get("cached_tokens", usage.get("cached_tokens")), "upstream_attempt_count": entry.get("upstream_attempt_count"), "request_body_bytes": entry.get("request_body_bytes"), "response_bytes_received": entry.get("response_bytes_received"), "stream_chunk_count": entry.get("stream_chunk_count"), "first_response_at": entry.get("first_response_at"), "first_response_delay_ms": entry.get("first_response_delay_ms"), "reasoning_retry_current_round": parse_int_value(entry.get("reasoning_retry_current_round")), "reasoning_retry_current_width": parse_int_value(entry.get("reasoning_retry_current_width")), "reasoning_retry_current_firsts": normalize_request_retry_firsts(entry.get("reasoning_retry_current_firsts")), "duration_ms": entry.get("duration_ms"), "started_at": entry.get("started_at"), "finished_at": entry.get("finished_at"), "last_activity_at": entry.get("last_activity_at"), "usage_last_updated_at": entry.get("usage_last_updated_at"), "error": str(entry.get("error") or ""), "upstream_origin": str(entry.get("upstream")["origin"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_origin") or ""), "upstream_path": str(entry.get("upstream")["path"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_path") or ""), "upstream_route": str(entry.get("upstream")["route"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_route") or ""), "upstream_auth_mode": str(entry.get("upstream")["auth_mode"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_auth_mode") or ""), "upstream_auth_source": str(entry.get("upstream")["auth_source"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_auth_source") or ""), "raw": entry, } ) rows.sort(key=lambda item: (item["seq"], item["request_id"]), reverse=True) return rows def render_request_detail(row: dict[str, Any]) -> str: bits = [ f"seq {row['seq']}", f"req {request_id_text(row)}", f"resp {response_id_text(row)}", f"thread {row.get('thread_id') or '-'}", f"started {short_time(row['started_at'])}", f"{row['method']} {row['path']}", f"profile {row.get('profile_name') or '-'}", f"status {request_status_label(row)}", f"upstream {row['upstream_status_code'] or '-'}", f"attempts {row['upstream_attempt_count'] or 0}", f"first {request_first_text(row)}", f"duration {format_duration_ms_as_seconds(row['duration_ms'])}", f"request {format_bytes(row.get('request_body_bytes'))}", f"response {format_bytes(row.get('response_bytes_received'))}", f"chunks {row['stream_chunk_count'] or 0}", f"life {request_lifecycle_label(row)}", f"finished {short_time(row['finished_at'])}", f"updated {request_updated_elapsed(row)}", ] retry_round = request_retry_round_text(row) if retry_round != "-": bits.append(f"retry round {retry_round}") if row.get("model"): bits.append(f"model {row['model']}") if row.get("requested_model") and row.get("requested_model") != row.get("model"): bits.append(f"requested {row['requested_model']}") if row.get("forwarded_model") and row.get("forwarded_model") != row.get("model"): bits.append(f"forwarded {row['forwarded_model']}") if row.get("reasoning_effort"): bits.append(f"effort {row['reasoning_effort']}") if row.get("reasoning_tokens") is not None: bits.append(f"reasoning {row['reasoning_tokens']}") if row.get("input_tokens") is not None or row.get("output_tokens") is not None or row.get("total_tokens") is not None or row.get("cached_tokens") is not None: bits.append(f"usage {request_usage_summary(row)} | total {format_count(row.get('total_tokens'))}") if row.get("cached_tokens") is not None: bits.append(f"cached raw {format_count(row['cached_tokens'])}") if row.get("error"): bits.append(f"error {row['error']}") if row.get("discard_reason"): bits.append(f"discard {row['discard_reason']}") if row.get("upstream_origin"): bits.append(f"origin {row['upstream_origin']}") if row.get("upstream_path"): bits.append(f"upstream path {row['upstream_path']}") if row.get("upstream_route"): bits.append(f"route {row['upstream_route']}") if row.get("upstream_auth_mode"): bits.append(f"auth {row['upstream_auth_mode']}/{row.get('upstream_auth_source') or '-'}") retry_wave = request_retry_wave_summary(row) if retry_wave: bits.append(f"current wave {retry_wave}") return " | ".join(bits) def normalize_profile_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]: needle = filter_text.strip().lower() rows = [] for profile in payload.get("profiles") or []: if not isinstance(profile, dict): continue summary = profile.get("summary") if isinstance(profile.get("summary"), dict) else {} haystack = " ".join( str(profile.get(key) or "") for key in ("name", "file_path", "active") ).lower() + " " + " ".join(str(summary.get(key) or "") for key in summary.keys()).lower() if needle and needle not in haystack: continue rows.append( { "name": str(profile.get("name") or ""), "active": bool(profile.get("active")), "file_path": str(profile.get("file_path") or ""), "listen_host": str(summary.get("listen_host") or "-"), "listen_port": str(summary.get("listen_port") or "-"), "upstream_base_url": str(summary.get("upstream_base_url") or "-"), "auth_mode": str(summary.get("auth_mode") or "-"), "auth_env": str(summary.get("auth_env") or "-"), "auth_file": str(summary.get("auth_file") or ""), "auth_json_path": str(summary.get("auth_json_path") or ""), "auth_json_key": str(summary.get("auth_json_key") or "-"), "request_history_limit": summary.get("request_history_limit"), "model_remap": str(summary.get("model_remap") or ""), "auth_source": str(summary.get("auth_source") or "-"), "reasoning_equals": summary.get("reasoning_equals"), "raw": profile, } ) rows.sort(key=lambda item: (not item["active"], item["name"].lower())) return rows def normalize_image_profile_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]: needle = filter_text.strip().lower() rows = [] for profile in payload.get("image_profiles") or []: if not isinstance(profile, dict): continue summary = profile.get("summary") if isinstance(profile.get("summary"), dict) else {} haystack = " ".join( str(profile.get(key) or "") for key in ("name", "file_path", "active") ).lower() + " " + " ".join(str(summary.get(key) or "") for key in summary.keys()).lower() if needle and needle not in haystack: continue rows.append( { "name": str(profile.get("name") or ""), "active": bool(profile.get("active")), "file_path": str(profile.get("file_path") or ""), "base_url": str(summary.get("base_url") or "-"), "auth_mode": str(summary.get("auth_mode") or "-"), "auth_env": str(summary.get("auth_env") or "-"), "auth_file": str(summary.get("auth_file") or ""), "manual_secret_file": str(summary.get("manual_secret_file") or ""), "auth_json_path": str(summary.get("auth_json_path") or ""), "auth_json_key": str(summary.get("auth_json_key") or "-"), "auth_source": str(summary.get("auth_source") or "disabled"), "raw": profile, } ) rows.sort(key=lambda item: (not item["active"], item["name"].lower())) return rows def render_profile_detail(row: dict[str, Any]) -> str: bits = [ f"{row['name']}", "active" if row["active"] else "inactive", f"listen {row['listen_host']}:{row['listen_port']}", f"text upstream {short_text(row['upstream_base_url'], 64)}", f"text auth {row['auth_mode']}/{row['auth_source']}", ] if row.get("request_history_limit") is not None: bits.append(f"history {row['request_history_limit']}") if row.get("reasoning_equals"): bits.append(f"reasoning {row['reasoning_equals']}") if row.get("model_remap"): bits.append(f"remap {short_text(row['model_remap'], 96)}") if row.get("auth_file"): bits.append("auth file configured") if row.get("auth_json_path"): bits.append("auth json configured") return " | ".join(bits) def render_image_profile_detail(row: dict[str, Any]) -> str: bits = [ f"image {row['name']}", "active" if row["active"] else "inactive", f"upstream {short_text(row['base_url'], 64)}", f"auth {row['auth_mode']}/{row['auth_source']}", ] if row.get("auth_file"): bits.append("auth file configured") if row.get("auth_json_path"): bits.append("auth json configured") return " | ".join(bits) def profile_form_state(row: dict[str, Any]) -> dict[str, Any]: raw = row.get("raw") if isinstance(row.get("raw"), dict) else {} form = raw.get("form") if isinstance(raw.get("form"), dict) else {} return { "name": row.get("name") or "", "listen_host": form.get("listen_host") or row.get("listen_host") or "", "listen_port": parse_int_value(form.get("listen_port") or row.get("listen_port")), "upstream_base_url": form.get("upstream_base_url") or row.get("upstream_base_url") or "", "auth_mode": form.get("auth_mode") or row.get("auth_mode") or "passthrough", "auth_env": form.get("auth_env") or "", "auth_file": form.get("auth_file") or "", "manual_secret": "", "manual_secret_file": form.get("manual_secret_file") or "", "manual_secret_configured": parse_bool_value(form.get("manual_secret_configured"), False), "auth_json_path": form.get("auth_json_path") or "", "auth_json_key": form.get("auth_json_key") or row.get("auth_json_key") or "", "request_history_limit": parse_int_value(form.get("request_history_limit") or row.get("request_history_limit")) if parse_int_value(form.get("request_history_limit") or row.get("request_history_limit")) is not None else DEFAULT_PROFILE_REQUEST_HISTORY_LIMIT, "model_remap": form.get("model_remap") or row.get("model_remap") or "", "reasoning_equals": normalize_editor_integer_list(form.get("reasoning_equals") or row.get("reasoning_equals")) or list(DEFAULT_PROFILE_REASONING_EQUALS), "retryable_status_codes": normalize_editor_integer_list(form.get("retryable_status_codes")) or list(DEFAULT_PROFILE_RETRYABLE_STATUS_CODES), "retryable_error_messages": normalize_editor_phrase_list(form.get("retryable_error_messages")) or list(DEFAULT_PROFILE_RETRYABLE_ERROR_MESSAGES), "upstream_fetch_retry_attempts": parse_int_value(form.get("upstream_fetch_retry_attempts")) if parse_int_value(form.get("upstream_fetch_retry_attempts")) is not None else DEFAULT_PROFILE_UPSTREAM_FETCH_RETRY_ATTEMPTS, "upstream_fetch_retry_backoff_ms": parse_int_value(form.get("upstream_fetch_retry_backoff_ms")) if parse_int_value(form.get("upstream_fetch_retry_backoff_ms")) is not None else DEFAULT_PROFILE_UPSTREAM_FETCH_RETRY_BACKOFF_MS, "endpoints": normalize_editor_string_list(form.get("endpoints")) or list(DEFAULT_PROFILE_ENDPOINTS), } def profile_payload_from_row(row: dict[str, Any]) -> dict[str, Any]: state = profile_form_state(row) return { "name": str(state.get("name") or "").strip(), "listen_host": str(state.get("listen_host") or "").strip(), "listen_port": state.get("listen_port"), "upstream_base_url": str(state.get("upstream_base_url") or "").strip(), "auth_mode": str(state.get("auth_mode") or "passthrough").strip(), "auth_env": str(state.get("auth_env") or "").strip(), "auth_file": str(state.get("auth_file") or "").strip(), "manual_secret": "", "manual_secret_file": str(state.get("manual_secret_file") or "").strip(), "manual_secret_configured": bool(state.get("manual_secret_configured")), "auth_json_path": str(state.get("auth_json_path") or "").strip(), "auth_json_key": str(state.get("auth_json_key") or "").strip(), "request_history_limit": state.get("request_history_limit"), "model_remap": str(state.get("model_remap") or "").strip(), "reasoning_equals": state.get("reasoning_equals") or [], "retryable_status_codes": state.get("retryable_status_codes") or [], "retryable_error_messages": state.get("retryable_error_messages") or [], "upstream_fetch_retry_attempts": state.get("upstream_fetch_retry_attempts"), "upstream_fetch_retry_backoff_ms": state.get("upstream_fetch_retry_backoff_ms"), "endpoints": state.get("endpoints") or [], } def profile_editor_document(row: dict[str, Any]) -> str: state = profile_form_state(row) model_remap = str(state.get("model_remap") or "") if "\n" in model_remap: model_remap_value = '"""\n' + model_remap.rstrip("\n").replace('"""', '\\"""') + '\n"""' else: model_remap_value = toml_string(model_remap) return "\n".join( [ "# Edit the selected codex-retry-gateway profile and save.", "# Leave manual_secret empty to keep the current secret file.", "# Changing name creates a new profile file; it does not delete the old one.", "", f"name = {toml_string(state.get('name'))}", f"listen_host = {toml_string(state.get('listen_host'))}", f"listen_port = {state.get('listen_port') if state.get('listen_port') is not None else 4610}", f"upstream_base_url = {toml_string(state.get('upstream_base_url'))}", f"auth_mode = {toml_string(state.get('auth_mode'))}", f"auth_env = {toml_string(state.get('auth_env'))}", f"auth_file = {toml_string(state.get('auth_file'))}", 'manual_secret = ""', f"manual_secret_file = {toml_string(state.get('manual_secret_file'))}", f"manual_secret_configured = {toml_bool(state.get('manual_secret_configured'))}", f"auth_json_path = {toml_string(state.get('auth_json_path'))}", f"auth_json_key = {toml_string(state.get('auth_json_key'))}", "", f"request_history_limit = {state.get('request_history_limit') if state.get('request_history_limit') is not None else 0}", f"model_remap = {model_remap_value}", f"reasoning_equals = {toml_int_list(state.get('reasoning_equals'))}", f"retryable_status_codes = {toml_int_list(state.get('retryable_status_codes'))}", f"retryable_error_messages = {toml_phrase_list(state.get('retryable_error_messages'))}", f"upstream_fetch_retry_attempts = {state.get('upstream_fetch_retry_attempts') if state.get('upstream_fetch_retry_attempts') is not None else 5}", f"upstream_fetch_retry_backoff_ms = {state.get('upstream_fetch_retry_backoff_ms') if state.get('upstream_fetch_retry_backoff_ms') is not None else 350}", f"endpoints = {toml_string_list(state.get('endpoints'))}", "", ] ) def profile_payload_from_editor_text(text: str) -> dict[str, Any]: data = tomllib.loads(text) if not isinstance(data, dict): raise ValueError("editor payload must be a TOML object") return { "name": str(data.get("name") or "").strip(), "listen_host": str(data.get("listen_host") or "").strip(), "listen_port": parse_int_value(data.get("listen_port")), "upstream_base_url": str(data.get("upstream_base_url") or "").strip(), "auth_mode": str(data.get("auth_mode") or "passthrough").strip(), "auth_env": str(data.get("auth_env") or "").strip(), "auth_file": str(data.get("auth_file") or "").strip(), "manual_secret": str(data.get("manual_secret") or "").strip(), "manual_secret_file": str(data.get("manual_secret_file") or "").strip(), "manual_secret_configured": parse_bool_value(data.get("manual_secret_configured"), False), "auth_json_path": str(data.get("auth_json_path") or "").strip(), "auth_json_key": str(data.get("auth_json_key") or "").strip(), "request_history_limit": parse_int_value(data.get("request_history_limit")), "model_remap": str(data.get("model_remap") or "").strip(), "reasoning_equals": normalize_editor_integer_list(data.get("reasoning_equals")), "retryable_status_codes": normalize_editor_integer_list(data.get("retryable_status_codes")), "retryable_error_messages": normalize_editor_phrase_list(data.get("retryable_error_messages")), "upstream_fetch_retry_attempts": parse_int_value(data.get("upstream_fetch_retry_attempts")), "upstream_fetch_retry_backoff_ms": parse_int_value(data.get("upstream_fetch_retry_backoff_ms")), "endpoints": normalize_editor_string_list(data.get("endpoints")), } def resolve_editor_command() -> list[str]: configured = (os.environ.get("VISUAL") or os.environ.get("EDITOR") or "").strip() if configured: return shlex.split(configured) for candidate in ("nano", "vim", "vi"): if shutil.which(candidate): return [candidate] raise RuntimeError("no editor found; set $VISUAL or $EDITOR") def edit_profile_payload_with_editor(row: dict[str, Any]) -> dict[str, Any] | None: original_text = profile_editor_document(row) editor_command = resolve_editor_command() fd, raw_path = tempfile.mkstemp(prefix=f"codex-retry-profile-{row.get('name') or 'profile'}-", suffix=".toml") os.close(fd) temp_path = Path(raw_path) temp_path.write_text(original_text, encoding="utf-8") try: result = subprocess.run(editor_command + [str(temp_path)], check=False) edited_text = temp_path.read_text(encoding="utf-8") if edited_text == original_text: return None payload = profile_payload_from_editor_text(edited_text) if result.returncode != 0: raise RuntimeError(f"editor exited with status {result.returncode}") return payload except Exception as exc: raise RuntimeError(f"{exc}; kept draft at {temp_path}") from exc finally: if temp_path.exists(): try: if temp_path.read_text(encoding="utf-8") == original_text: temp_path.unlink() except Exception: pass def image_profile_form_state(row: dict[str, Any]) -> dict[str, Any]: raw = row.get("raw") if isinstance(row.get("raw"), dict) else {} form = raw.get("form") if isinstance(raw.get("form"), dict) else {} return { "name": row.get("name") or "", "base_url": form.get("base_url") or row.get("base_url") or "", "auth_mode": form.get("auth_mode") or row.get("auth_mode") or "fixed_bearer", "auth_env": form.get("auth_env") or "CODEX_RETRY_GATEWAY_IMAGE_API_KEY", "auth_file": form.get("auth_file") or "", "manual_secret": "", "manual_secret_file": form.get("manual_secret_file") or "", "manual_secret_configured": parse_bool_value(form.get("manual_secret_configured"), False), "auth_json_path": form.get("auth_json_path") or "", "auth_json_key": form.get("auth_json_key") or "OPENAI_API_KEY", } def image_profile_payload_from_row(row: dict[str, Any]) -> dict[str, Any]: state = image_profile_form_state(row) return { "name": str(state.get("name") or "").strip(), "base_url": str(state.get("base_url") or "").strip(), "auth_mode": str(state.get("auth_mode") or "fixed_bearer").strip(), "auth_env": str(state.get("auth_env") or "").strip(), "auth_file": str(state.get("auth_file") or "").strip(), "manual_secret": "", "manual_secret_file": str(state.get("manual_secret_file") or "").strip(), "manual_secret_configured": bool(state.get("manual_secret_configured")), "auth_json_path": str(state.get("auth_json_path") or "").strip(), "auth_json_key": str(state.get("auth_json_key") or "").strip(), } def image_profile_editor_document(row: dict[str, Any]) -> str: state = image_profile_form_state(row) return "\n".join( [ "# Edit the selected codex-retry-gateway image profile and save.", "# base_url applies to both /images/* and /v1/images/*.", "# Leave manual_secret empty to keep the current image secret file.", "# Changing name creates a new image profile file; it does not delete the old one.", "", f"name = {toml_string(state.get('name'))}", f"base_url = {toml_string(state.get('base_url'))}", f"auth_mode = {toml_string(state.get('auth_mode'))}", f"auth_env = {toml_string(state.get('auth_env'))}", f"auth_file = {toml_string(state.get('auth_file'))}", 'manual_secret = ""', f"manual_secret_file = {toml_string(state.get('manual_secret_file'))}", f"manual_secret_configured = {toml_bool(state.get('manual_secret_configured'))}", f"auth_json_path = {toml_string(state.get('auth_json_path'))}", f"auth_json_key = {toml_string(state.get('auth_json_key'))}", "", ] ) def image_profile_payload_from_editor_text(text: str) -> dict[str, Any]: data = tomllib.loads(text) if not isinstance(data, dict): raise ValueError("editor payload must be a TOML object") return { "name": str(data.get("name") or "").strip(), "base_url": str(data.get("base_url") or "").strip(), "auth_mode": str(data.get("auth_mode") or "fixed_bearer").strip(), "auth_env": str(data.get("auth_env") or "").strip(), "auth_file": str(data.get("auth_file") or "").strip(), "manual_secret": str(data.get("manual_secret") or "").strip(), "manual_secret_file": str(data.get("manual_secret_file") or "").strip(), "manual_secret_configured": parse_bool_value(data.get("manual_secret_configured"), False), "auth_json_path": str(data.get("auth_json_path") or "").strip(), "auth_json_key": str(data.get("auth_json_key") or "").strip(), } def edit_image_profile_payload_with_editor(row: dict[str, Any]) -> dict[str, Any] | None: original_text = image_profile_editor_document(row) editor_command = resolve_editor_command() fd, raw_path = tempfile.mkstemp(prefix=f"codex-retry-image-profile-{row.get('name') or 'profile'}-", suffix=".toml") os.close(fd) temp_path = Path(raw_path) temp_path.write_text(original_text, encoding="utf-8") try: result = subprocess.run(editor_command + [str(temp_path)], check=False) edited_text = temp_path.read_text(encoding="utf-8") if edited_text == original_text: return None payload = image_profile_payload_from_editor_text(edited_text) if result.returncode != 0: raise RuntimeError(f"editor exited with status {result.returncode}") return payload except Exception as exc: raise RuntimeError(f"{exc}; kept draft at {temp_path}") from exc finally: if temp_path.exists(): try: if temp_path.read_text(encoding="utf-8") == original_text: temp_path.unlink() except Exception: pass def edit_request_table_preferences_with_editor(preferences: dict[str, Any]) -> dict[str, Any] | None: original_text = request_table_preferences_document(preferences) editor_command = resolve_editor_command() fd, raw_path = tempfile.mkstemp(prefix="codex-retry-columns-", suffix=".txt") os.close(fd) temp_path = Path(raw_path) temp_path.write_text(original_text, encoding="utf-8") try: result = subprocess.run(editor_command + [str(temp_path)], check=False) edited_text = temp_path.read_text(encoding="utf-8") if edited_text == original_text: return None preferences = request_table_preferences_from_text(edited_text) if result.returncode != 0: raise RuntimeError(f"editor exited with status {result.returncode}") return preferences except Exception as exc: raise RuntimeError(f"{exc}; kept draft at {temp_path}") from exc finally: if temp_path.exists(): try: if temp_path.read_text(encoding="utf-8") == original_text: temp_path.unlink() except Exception: pass def action_url(api_url: str, suffix: str) -> str: base = api_url.rstrip("/") return f"{base}{suffix}" def post_json(url: str, timeout: int, payload: dict[str, Any], access_key: str = "") -> tuple[int, dict[str, Any]]: req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={**request_headers(access_key), "Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=timeout) as response: data = json.loads(response.read().decode("utf-8")) if not isinstance(data, dict): raise RuntimeError("API did not return a JSON object") return response.status, data def delete_json(url: str, timeout: int, access_key: str = "") -> tuple[int, dict[str, Any]]: req = urllib.request.Request(url, headers=request_headers(access_key), method="DELETE") with urllib.request.urlopen(req, timeout=timeout) as response: data = json.loads(response.read().decode("utf-8")) if not isinstance(data, dict): raise RuntimeError("API did not return a JSON object") return response.status, data def open_url(url: str) -> None: if sys.platform == "darwin": command = ["open", url] elif sys.platform == "win32": command = ["cmd", "/c", "start", "", url] else: command = ["xdg-open", url] subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def fetch_dashboard_snapshot( gateway_url: str, status_url: str, timeout: int, *, access_key: str = "", filter_text: str = "", current_view: str = "overview", ) -> dict[str, Any]: gateway_root = gateway_admin_url(gateway_url) endpoints = { "status": status_url or gateway_status_url(gateway_root), "logs": build_api_url(gateway_root, "/api/logs", {"limit": 200}), "requests": build_api_url(gateway_root, "/api/requests", {"limit": 200}), "profiles": build_api_url(gateway_root, "/api/profiles"), "image_profiles": build_api_url(gateway_root, "/api/image-profiles"), } 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, allow_not_found=name == "image_profiles") for name, url in endpoints.items() } status_payload, status_error = futures["status"].result() logs_payload, logs_error = futures["logs"].result() requests_payload, requests_error = futures["requests"].result() profiles_payload, profiles_error = futures["profiles"].result() image_profiles_payload, image_profiles_error = futures["image_profiles"].result() payload = status_payload if isinstance(status_payload, dict) else {} requests_source = requests_payload if isinstance(requests_payload, dict) else {} logs_source = logs_payload if isinstance(logs_payload, dict) else {} profiles_source = profiles_payload if isinstance(profiles_payload, dict) else {} image_profiles_source = image_profiles_payload if isinstance(image_profiles_payload, dict) else {} errors = [message for message in (status_error, logs_error, requests_error, profiles_error, image_profiles_error) if message] return { "payload": payload, "status_payload": payload, "status_error": " | ".join(errors), "requests": normalize_request_rows(requests_source, filter_text), "logs": normalize_log_rows(logs_source), "profiles": normalize_profile_rows( profiles_source, filter_text if current_view == "profiles" else "", ), "image_profiles": normalize_image_profile_rows( image_profiles_source, filter_text if current_view == "profiles" else "", ), "active_profile_name": ( str( payload.get("config", {}).get("profile_name") or payload.get("state", {}).get("profile_name") or profiles_source.get("active_profile") or "" ).strip() ), "active_image_profile_name": ( str( payload.get("config", {}).get("image_profile_name") or payload.get("state", {}).get("image_profile_name") or image_profiles_source.get("active_image_profile") or "" ).strip() ), } def run_textual( api_url: str, status_url: str, refresh_seconds: int, timeout: int, version_message: str = "", access_key: str = "", ) -> int: try: from textual.app import App, ComposeResult from textual.coordinate import Coordinate from textual.widgets import DataTable, Footer, Header, Input, Static except ImportError: print("Textual is required. Run with: uv run --with textual python codex_retry_gateway_tui.py", file=sys.stderr) return 2 gateway_base_url = gateway_admin_url(api_url) status_api_url = status_url or gateway_status_url(gateway_base_url) class CodexRetryGatewayTui(App[None]): CSS = """ Screen { layout: vertical; } #summary { height: 1; padding: 0 1; color: $accent; } #paths { height: 1; padding: 0 1; color: $text-muted; } #controls { height: 1; padding: 0 1; color: $text-muted; } #tables { height: 1; padding: 0 1; color: $text-muted; } #requests_table, #logs_table, #profiles_table { height: 1fr; } #detail { height: 3; padding: 0 1; border-top: solid $panel; } #status { height: 1; padding: 0 1; color: $text-muted; } """ BINDINGS = [ ("q", "quit", "Quit"), ("r", "refresh", "Refresh"), ("1", "show_overview", "Overview"), ("2", "show_requests", "Requests"), ("3", "show_logs", "Logs"), ("4", "show_profiles", "Profiles"), ("i", "toggle_profile_kind", "Text/Image"), ("m", "toggle_request_table_density", "Wide/Compact"), ("c", "edit_request_columns", "Columns"), ("shift+left", "scroll_table_left", "Scroll Left"), ("shift+right", "scroll_table_right", "Scroll Right"), ("e", "edit_profile", "Edit Profile"), ("p", "probe_profile", "Probe Profile"), ("s", "switch_profile", "Switch Profile"), ("w", "save_active_profile", "Save Profile"), ("d", "delete_profile", "Delete Profile"), ("u", "open_upstream", "Open Upstream"), ("/", "focus_filter", "Filter"), ] def __init__(self) -> None: super().__init__() self.payload: dict[str, Any] = {} self.status_payload: dict[str, Any] = {} self.status_error = "" self.requests: list[dict[str, Any]] = [] self.logs: list[dict[str, Any]] = [] self.profiles: list[dict[str, Any]] = [] self.image_profiles: list[dict[str, Any]] = [] self.request_by_key: dict[str, dict[str, Any]] = {} self.profile_by_key: dict[str, dict[str, Any]] = {} self.image_profile_by_key: dict[str, dict[str, Any]] = {} self.log_by_key: dict[str, dict[str, Any]] = {} self.current_view = "overview" self.filter_mode = "requests" self.last_request_key = "" self.loading = False self.executor = ThreadPoolExecutor(max_workers=1) self.refresh_generation = 0 self.active_profile_name = "" self.active_image_profile_name = "" self.profile_kind = "text" self.visible_tables = { "overview": "requests", "requests": "requests", "logs": "logs", "profiles": "profiles", } self.request_table_column_keys: dict[str, Any] = {} self.request_table_preferences = load_request_table_preferences() self.request_table_density = "compact" self.request_table_width_profiles = { density: dict(widths) for density, widths in REQUEST_TABLE_WIDTH_PROFILES.items() } def compose(self) -> ComposeResult: yield Header(show_clock=True) yield Static("", id="summary") yield Static("", id="paths") yield Input(placeholder="requests filter", id="filter") yield Static("", id="controls") yield Static("", id="tables") yield DataTable(id="requests_table") yield DataTable(id="logs_table") yield DataTable(id="profiles_table") yield Static("", id="detail") yield Static("", id="status") yield Footer() def on_mount(self) -> None: requests = self.query_one("#requests_table", DataTable) requests.cursor_type = "row" requests.zebra_stripes = True self._rebuild_request_table_columns() logs = self.query_one("#logs_table", DataTable) logs.cursor_type = "row" logs.zebra_stripes = True logs.add_columns("Seq", "At", "Message") profiles = self.query_one("#profiles_table", DataTable) profiles.cursor_type = "row" profiles.zebra_stripes = True self._rebuild_profile_table_columns() self._set_view("overview") self.refresh_data(refresh=True) self.set_interval(refresh_seconds, self.refresh_data) def on_unmount(self) -> None: self.executor.shutdown(wait=False, cancel_futures=True) def _set_view(self, view: str) -> None: self.current_view = view active_table = self.visible_tables.get(view, "requests") self.query_one("#requests_table").display = active_table == "requests" self.query_one("#logs_table").display = active_table == "logs" self.query_one("#profiles_table").display = active_table == "profiles" self.query_one("#controls", Static).update( "views: 1 overview | 2 requests | 3 logs | 4 profiles | i text/image | / filter | r refresh | m compact/wide | c columns | Shift-Left/Right scroll | e edit | p probe | s switch | w save | d delete | u open upstream" ) self._focus_table() self._update_table_headers() def _focus_table(self) -> None: table = self._current_table_widget() if table: table.focus() def _update_summary(self) -> None: self.query_one("#summary", Static).update(summary_line(self.payload)) self.query_one("#paths", Static).update(state_summary(self.payload)) def _update_table_headers(self) -> None: visible_columns = request_table_visible_columns(self.request_table_preferences) counts = ( f"requests {len(self.requests)} | logs {len(self.logs)} | text profiles {len(self.profiles)} | image profiles {len(self.image_profiles)} | active {self.profile_kind} | " f"req cols {len(visible_columns)}/{len(REQUEST_TABLE_COLUMNS)} | c edit columns" ) self.query_one("#tables", Static).update(counts) def _current_filter(self) -> str: return self.query_one("#filter", Input).value.strip() def _visible_request_column_keys(self) -> list[str]: return request_table_visible_columns(self.request_table_preferences) def _request_table_actions_enabled(self) -> bool: if self.visible_tables.get(self.current_view, "requests") == "requests": return True self._set_status("switch to requests or overview first") return False def _persist_request_table_preferences(self) -> bool: self.request_table_preferences = normalize_request_table_preferences(self.request_table_preferences) try: write_request_table_preferences(self.request_table_preferences) except Exception as exc: self._set_status(f"save request table prefs error: {exc}") return False return True def _request_table_column_width(self, column_key: str, density: str | None = None) -> int: normalized = normalize_request_table_preferences(self.request_table_preferences) override = request_table_column_width_value(normalized.get("widths", {}).get(column_key)) if override is not None: return override active_density = density or self.request_table_density width_profile = self.request_table_width_profiles[active_density] column = REQUEST_TABLE_COLUMN_BY_KEY[column_key] return width_profile[column["width_key"]] def _request_row_key(self, row: dict[str, Any]) -> str: return f"{row['seq']}:{row['request_id']}" def _selected_request_key(self) -> str: row = self._selected_request_row() if row: return self._request_row_key(row) return self.last_request_key def _select_request_row(self, row_key: str) -> bool: if not row_key: return False table = self.query_one("#requests_table", DataTable) for index, row in enumerate(self.requests): if self._request_row_key(row) == row_key: return self._select_table_row(table, index) return False def _rebuild_request_table_columns(self) -> None: table = self.query_one("#requests_table", DataTable) selected_row_index = table.cursor_row if table.cursor_row is not None and table.cursor_row >= 0 else 0 selected_row_key = self._selected_request_key() table.clear(columns=True) self.request_table_column_keys = {} for column_key in self._visible_request_column_keys(): column = REQUEST_TABLE_COLUMN_BY_KEY[column_key] width = self._request_table_column_width(column_key) self.request_table_column_keys[column_key] = table.add_column( column["label"], width=width, key=column_key, ) if self.requests: self.render_requests() if not self._select_request_row(selected_row_key): self._select_table_row(table, min(selected_row_index, len(self.requests) - 1)) else: self.request_by_key = {} def _profile_rows(self) -> list[dict[str, Any]]: return self.image_profiles if self.profile_kind == "image" else self.profiles def _active_profile_name(self) -> str: return self.active_image_profile_name if self.profile_kind == "image" else self.active_profile_name def _profile_api_base(self) -> str: return "/api/image-profiles" if self.profile_kind == "image" else "/api/profiles" def _profile_kind_label(self) -> str: return "image profile" if self.profile_kind == "image" else "text profile" def _rebuild_profile_table_columns(self) -> None: table = self.query_one("#profiles_table", DataTable) table.clear(columns=True) if self.profile_kind == "image": table.add_columns("Name", "Active", "Image Upstream", "Image Auth", "Source") else: table.add_columns( "Name", "Active", "Listen", "Text Upstream", "Text Auth", "History", "Reasoning", "Source", ) def _current_table_widget(self) -> DataTable | None: table_id = self.visible_tables.get(self.current_view, "requests") if table_id == "logs": return self.query_one("#logs_table", DataTable) if table_id == "profiles": return self.query_one("#profiles_table", DataTable) return self.query_one("#requests_table", DataTable) def _select_table_row(self, table: DataTable, row_index: int) -> bool: if row_index < 0 or row_index >= table.row_count: return False table.cursor_coordinate = Coordinate(row_index, 0) return True def _select_profile_row(self, profile_name: str) -> bool: table = self.query_one("#profiles_table", DataTable) rows = self._profile_rows() if not rows: return False if profile_name: for index, row in enumerate(rows): if row["name"] == profile_name: return self._select_table_row(table, index) return self._select_table_row(table, 0) def _selected_request_row(self) -> dict[str, Any] | None: table = self.query_one("#requests_table", DataTable) if table.cursor_row is None or table.cursor_row < 0: return None if table.cursor_row >= len(self.requests): return None return self.requests[table.cursor_row] def _selected_profile_row(self) -> dict[str, Any] | None: table = self.query_one("#profiles_table", DataTable) if table.cursor_row is None or table.cursor_row < 0: return None rows = self._profile_rows() if table.cursor_row >= len(rows): return None return rows[table.cursor_row] def _selected_log_row(self) -> dict[str, Any] | None: table = self.query_one("#logs_table", DataTable) if table.cursor_row is None or table.cursor_row < 0: return None if table.cursor_row >= len(self.logs): return None return self.logs[table.cursor_row] def _set_status(self, text: str) -> None: self.query_one("#status", Static).update(text) def refresh_data(self, refresh: bool = False) -> None: if self.loading: return self.loading = True self.refresh_generation += 1 generation = self.refresh_generation self._set_status("refreshing...") current_filter = self._current_filter() self.executor.submit(self._refresh_in_worker, generation, current_filter) def _refresh_in_worker(self, generation: int, current_filter: str) -> None: try: result = fetch_dashboard_snapshot( gateway_base_url, status_api_url, timeout, access_key=access_key, filter_text=current_filter, current_view=self.current_view, ) result["generation"] = generation self.call_from_thread(self._apply_refresh_result, result) except Exception as exc: self.call_from_thread(self._apply_refresh_error, generation, exc) def _apply_refresh_result(self, result: dict[str, Any]) -> None: if result.get("generation") != self.refresh_generation: return selected_request_key = self._selected_request_key() self.payload = result["payload"] self.status_payload = result["status_payload"] self.status_error = result["status_error"] self.requests = result["requests"] self.logs = result["logs"] self.profiles = result["profiles"] self.image_profiles = result["image_profiles"] self.active_profile_name = result.get("active_profile_name") or "" self.active_image_profile_name = result.get("active_image_profile_name") or "" self.last_request_key = selected_request_key self.loading = False self.render_all() if self.current_view == "profiles": if not self._select_profile_row(self._active_profile_name()): self._select_profile_row("") version_bits = [version_message] if version_message else [] version_bits.append(status_summary(self.payload, self.status_error)) self._set_status(" | ".join(bit for bit in version_bits if bit)) def _apply_refresh_error(self, generation: int, exc: Exception) -> None: if generation != self.refresh_generation: return self.loading = False self._set_status(f"error: {exc}") def render_all(self) -> None: self._update_summary() self.render_requests() self.render_logs() self.render_profiles() if self.current_view == "requests": row = self._selected_request_row() if row: self.render_detail(row, kind="request") else: self.query_one("#detail", Static).update("no requests") elif self.current_view == "logs": row = self._selected_log_row() if row: self.render_detail(row, kind="log") else: self.query_one("#detail", Static).update("no logs") elif self.current_view == "profiles": row = self._selected_profile_row() if row: self.render_detail(row, kind="image_profile" if self.profile_kind == "image" else "profile") else: self.query_one("#detail", Static).update("no profiles") def render_requests(self) -> None: table = self.query_one("#requests_table", DataTable) selected_row_index = table.cursor_row if table.cursor_row is not None and table.cursor_row >= 0 else 0 selected_row_key = self._selected_request_key() text_limits = self._request_text_limits(table) visible_column_keys = self._visible_request_column_keys() table.clear() self.request_by_key = {} for row in self.requests: key = self._request_row_key(row) self.request_by_key[key] = row cells = request_row_cells(row, text_limits) visible_cells = request_visible_cells(row, cells, visible_column_keys) table.add_row(*visible_cells, key=key) if self.requests: if not self._select_request_row(selected_row_key): self._select_table_row(table, min(selected_row_index, len(self.requests) - 1)) def render_logs(self) -> None: table = self.query_one("#logs_table", DataTable) table.clear() self.log_by_key = {} for row in self.logs: key = str(row["seq"]) self.log_by_key[key] = row table.add_row(str(row["seq"]), short_time(row["at"]), short_text(row["message"], 80), key=key) def render_profiles(self) -> None: table = self.query_one("#profiles_table", DataTable) table.clear() rows = self._profile_rows() if self.profile_kind == "image": self.image_profile_by_key = {} else: self.profile_by_key = {} for row in rows: key = row["name"] if self.profile_kind == "image": self.image_profile_by_key[key] = row table.add_row( row["name"], "yes" if row["active"] else "", short_text(row["base_url"], 36), f"{row['auth_mode']}/{row['auth_source']}", short_text(row["file_path"], 36), key=key, ) else: self.profile_by_key[key] = row table.add_row( row["name"], "yes" if row["active"] else "", f"{row['listen_host']}:{row['listen_port']}", short_text(row["upstream_base_url"], 28), f"{row['auth_mode']}/{row['auth_source']}", str(row["request_history_limit"] if row["request_history_limit"] is not None else "-"), short_text(row["reasoning_equals"], 12), short_text(row["file_path"], 32), key=key, ) if self.current_view == "profiles" and self._active_profile_name(): self._select_profile_row(self._active_profile_name()) def render_detail(self, row: dict[str, Any], *, kind: str) -> None: if kind == "request": self.query_one("#detail", Static).update(render_request_detail(row)) elif kind == "profile": self.query_one("#detail", Static).update(render_profile_detail(row)) elif kind == "image_profile": self.query_one("#detail", Static).update(render_image_profile_detail(row)) else: message = str(row.get("message") or "").strip() self.query_one("#detail", Static).update(f"seq {row['seq']} | {short_time(row['at'])} | {message}") def action_refresh(self) -> None: self.refresh_data(refresh=True) def action_focus_filter(self) -> None: self.query_one("#filter", Input).focus() def action_show_overview(self) -> None: self._set_view("overview") def action_show_requests(self) -> None: self._set_view("requests") def action_show_logs(self) -> None: self._set_view("logs") def action_show_profiles(self) -> None: self._set_view("profiles") if not self._select_profile_row(self._active_profile_name()): self._select_profile_row("") def action_toggle_profile_kind(self) -> None: self.profile_kind = "image" if self.profile_kind == "text" else "text" self._rebuild_profile_table_columns() self.render_profiles() if self.current_view == "profiles": if not self._select_profile_row(self._active_profile_name()): self._select_profile_row("") row = self._selected_profile_row() if row: self.render_detail(row, kind="image_profile" if self.profile_kind == "image" else "profile") self._update_table_headers() self._set_status(f"profile view {self.profile_kind}") def action_newest_request(self) -> None: if self.requests: self.render_detail(self.requests[0], kind="request") def action_focus_requests(self) -> None: self._set_view("requests") def action_focus_logs(self) -> None: self._set_view("logs") def action_focus_profiles(self) -> None: self._set_view("profiles") def _request_text_limits(self, table: DataTable) -> dict[str, int]: width_profile = self.request_table_width_profiles[self.request_table_density] limits = { "req_id": width_profile["req_id"], "resp_id": width_profile["resp_id"], "thread": width_profile["thread"], "path": width_profile["path"], "model": width_profile["model"], "effort": width_profile["effort"], "first": width_profile["first"], "note": width_profile["note"], "round": width_profile["round"], } for name in ("req_id", "resp_id", "thread", "path", "model", "effort", "first", "note", "round"): key = self.request_table_column_keys.get(name) column = table.columns.get(key) if key is not None else None if column is None or not getattr(column, "width", 0): continue limits[name] = int(column.width) return limits def _rerender_requests_after_layout_change(self) -> None: self.render_requests() self._update_table_headers() if self.current_view not in {"overview", "requests"}: return row = self._selected_request_row() if row: self.render_detail(row, kind="request") else: self.query_one("#detail", Static).update("no requests") def _apply_request_table_width_profile(self, density: str) -> None: table = self.query_one("#requests_table", DataTable) for name, key in self.request_table_column_keys.items(): column = table.columns.get(key) if column is None: continue column.width = self._request_table_column_width(name, density) self.request_table_density = density self._rerender_requests_after_layout_change() self._set_status(f"request table mode {density}") def action_toggle_request_table_density(self) -> None: next_density = "wide" if self.request_table_density == "compact" else "compact" self._apply_request_table_width_profile(next_density) def action_edit_request_columns(self) -> None: try: with self.suspend(): updated = edit_request_table_preferences_with_editor(self.request_table_preferences) if updated is None: self._set_status("request columns unchanged") return self.request_table_preferences = updated if not self._persist_request_table_preferences(): return self._rebuild_request_table_columns() self._rerender_requests_after_layout_change() self._set_status( f"request columns saved -> {request_table_preferences_file_path()} | " f"{len(self._visible_request_column_keys())}/{len(REQUEST_TABLE_COLUMNS)} shown" ) except Exception as exc: self._set_status(f"request columns error: {exc}") def action_scroll_table_left(self) -> None: table = self._current_table_widget() if table: table.action_scroll_left() def action_scroll_table_right(self) -> None: table = self._current_table_widget() if table: table.action_scroll_right() def action_open_upstream(self) -> None: row = self._selected_profile_row() if not row: return upstream_value = row.get("base_url") if self.profile_kind == "image" else row.get("upstream_base_url") upstream = str(upstream_value or "").strip() if not upstream or upstream == "-": return open_url(upstream) def action_edit_profile(self) -> None: if self.current_view != "profiles": self._set_status("switch to profiles view first") return row = self._selected_profile_row() if not row: self._set_status("no profile selected") return try: with self.suspend(): payload = ( edit_image_profile_payload_with_editor(row) if self.profile_kind == "image" else edit_profile_payload_with_editor(row) ) if payload is None: self._set_status(f"edit {row['name']} cancelled (no changes)") return status, result = post_json(build_api_url(gateway_base_url, self._profile_api_base()), timeout, payload, access_key) self._set_status(f"edit {payload.get('name') or row['name']} -> {status} | {short_text(result, 96)}") self.refresh_data(refresh=True) except Exception as exc: self._set_status(f"edit error: {exc}") def action_probe_profile(self) -> None: row = self._selected_profile_row() if not row: return try: status, payload = post_json( build_api_url(gateway_base_url, f"{self._profile_api_base()}/probe"), timeout, {"profile": row["name"]}, access_key, ) self._set_status(f"probe {row['name']} -> {status} | {short_text(payload, 96)}") except Exception as exc: self._set_status(f"probe error: {exc}") def action_switch_profile(self) -> None: row = self._selected_profile_row() if not row: return try: status, payload = post_json( build_api_url(gateway_base_url, f"{self._profile_api_base()}/switch"), timeout, {"profile": row["name"]}, access_key, ) self._set_status(f"switch {row['name']} -> {status} | {short_text(payload, 96)}") self.refresh_data(refresh=True) except Exception as exc: self._set_status(f"switch error: {exc}") def action_save_active_profile(self) -> None: row = self._selected_profile_row() if not row: return payload = image_profile_payload_from_row(row) if self.profile_kind == "image" else profile_payload_from_row(row) try: status, result = post_json(build_api_url(gateway_base_url, self._profile_api_base()), timeout, payload, access_key) self._set_status(f"save profile -> {status} | {short_text(result, 96)}") self.refresh_data(refresh=True) except Exception as exc: self._set_status(f"save error: {exc}") def action_delete_profile(self) -> None: row = self._selected_profile_row() if not row or row["active"]: return try: status, payload = delete_json( build_api_url(gateway_base_url, f"{self._profile_api_base()}/{urllib.parse.quote(row['name'])}"), timeout, access_key, ) self._set_status(f"delete {row['name']} -> {status} | {short_text(payload, 96)}") self.refresh_data(refresh=True) except Exception as exc: self._set_status(f"delete error: {exc}") def on_input_changed(self, event: Input.Changed) -> None: if event.input.id == "filter": self.refresh_data(refresh=False) def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None: if event.data_table.id == "requests_table": key = str(event.row_key.value) self.last_request_key = key row = self.request_by_key.get(key) if row: self.render_detail(row, kind="request") elif event.data_table.id == "logs_table": key = str(event.row_key.value) row = self.log_by_key.get(key) if row: self.render_detail(row, kind="log") elif event.data_table.id == "profiles_table": key = str(event.row_key.value) row = self.image_profile_by_key.get(key) if self.profile_kind == "image" else self.profile_by_key.get(key) if row: self.render_detail(row, kind="image_profile" if self.profile_kind == "image" else "profile") CodexRetryGatewayTui().run() return 0 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Codex Retry Gateway TUI") parser.add_argument("--api-url", default=default_api_url()) parser.add_argument( "--access-key", default=default_access_key(), help="optional management access key for protected gateway admin pages and APIs", ) parser.add_argument( "--status-url", default=default_status_url(), help="optional status API URL; leave empty to infer from --api-url", ) parser.add_argument("--save-config", action="store_true", help="persist --api-url to ~/.config/codex-retry-gateway-tui/api-url before running") parser.add_argument("--install", action="store_true", help="persist --api-url, install codex-retry-gateway-tui as a uv tool, then exit") parser.add_argument("--version-check-url", default=os.environ.get("CODEX_RETRY_GATEWAY_TUI_VERSION_CHECK_URL", DEFAULT_VERSION_CHECK_URL)) parser.add_argument( "--version-check-timeout", type=int, default=env_int("CODEX_RETRY_GATEWAY_TUI_VERSION_CHECK_TIMEOUT", DEFAULT_VERSION_CHECK_TIMEOUT_SECONDS), ) parser.add_argument( "--no-version-check", action="store_true", default=os.environ.get("CODEX_RETRY_GATEWAY_TUI_NO_VERSION_CHECK", "").strip().lower() in {"1", "true", "yes", "on"}, ) parser.add_argument( "--refresh-seconds", type=int, default=env_int("CODEX_RETRY_GATEWAY_TUI_REFRESH_SECONDS", DEFAULT_REFRESH_SECONDS), ) parser.add_argument("--timeout", type=int, default=env_int("CODEX_RETRY_GATEWAY_TUI_TIMEOUT", DEFAULT_TIMEOUT_SECONDS)) parser.add_argument("--once", action="store_true", help="print one snapshot and exit") parser.add_argument("--filter", default="", help="initial filter for one-shot text output") return parser def normalize_log_rows(payload: dict[str, Any]) -> list[dict[str, Any]]: rows = [] for entry in payload.get("entries") or []: if not isinstance(entry, dict): continue rows.append( { "seq": as_int(entry.get("seq")), "at": entry.get("at"), "message": str(entry.get("message") or ""), "raw": entry, } ) rows.sort(key=lambda item: item["seq"], reverse=True) return rows def print_once(snapshot: dict[str, Any], filter_text: str = "") -> None: payload = snapshot.get("payload") if isinstance(snapshot, dict) and isinstance(snapshot.get("payload"), dict) else {} status_payload = snapshot.get("status_payload") if isinstance(snapshot, dict) and isinstance(snapshot.get("status_payload"), dict) else {} status_error = str(snapshot.get("status_error") or "") if isinstance(snapshot, dict) else "" print(summary_line(payload)) print(state_summary(payload)) if status_payload or status_error: print(status_summary(status_payload, status_error)) print("requests") for row in snapshot.get("requests") or []: try: print( f"{row['seq']:>6} {short_text(request_id_text(row), 16):<16} {short_text(response_id_text(row), 16):<16} {short_text(row.get('thread_id'), 14):<14} {short_time(row.get('started_at')):<19} {short_text(request_status_label(row), 14):<14} " f"{short_text(row['path'], 20):<20} {short_text(row['model'] or row['requested_model'] or row['forwarded_model'], 16):<16} {short_text(request_effort_text(row), 6):<6} {request_reasoning_tokens_text(row):<6} " f"{short_text(request_usage_summary(row), 26):<26} {format_bytes(row.get('request_body_bytes')):<8} {request_chunk_progress(row):<14} {format_duration_ms_as_seconds(row['duration_ms']):<8} {request_updated_elapsed(row):<8} " f"{short_text(request_retry_note(row), 30)}" ) except BrokenPipeError: return def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) args.api_url = gateway_admin_url(args.api_url) access_key = str(args.access_key or "").strip() status_url = str(args.status_url or "").strip() or default_status_url() if args.save_config or args.install: config_path = write_api_url_config(args.api_url) print(f"saved api url to {config_path}") if access_key: key_path = write_access_key_config(access_key) print(f"saved access key to {key_path}") if args.install: print("installing codex-retry-gateway-tui with uv tool...") return run_install_command() version_message = check_version_update( args.version_check_url, max(1, args.version_check_timeout), disabled=bool(args.no_version_check), ) if args.once: if version_message: print(version_message) snapshot = fetch_dashboard_snapshot( args.api_url, status_url, args.timeout, access_key=access_key, filter_text=args.filter, current_view="overview", ) print_once(snapshot, args.filter) return 0 return run_textual( args.api_url, status_url, max(1, args.refresh_seconds), max(1, args.timeout), version_message, access_key, ) if __name__ == "__main__": raise SystemExit(main())