Files
shusub2/sub2api_quota_tui.py
T

2619 lines
105 KiB
Python

#!/usr/bin/env python3
"""Textual TUI for Sub2API quota and daily account usage."""
from __future__ import annotations
import argparse
import datetime as dt
import gzip
import importlib.metadata
import json
import math
import os
import re
import subprocess
import sys
import urllib.parse
import urllib.request
import zlib
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any
APP_NAME = "shusub2"
FALLBACK_VERSION = "0.2.15"
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
DEFAULT_PRICING_URL = "https://price.tailbeb9ad.ts.net/api/ui-data?view=accounts"
DEFAULT_PRICING_URL_CONFIG_FILE = "~/.config/shusub2/pricing-url"
DEFAULT_LOGS_API_URL = "https://sub2apicn.shujk.top/api/v1/admin/usage"
DEFAULT_LOGS_URL_CONFIG_FILE = "~/.config/shusub2/logs-url"
DEFAULT_LOGS_TOKEN_CONFIG_FILE = "~/.config/shusub2/logs-token"
# us.sub2.shujk.top is the DNS-only standard HTTPS entry for server4, avoiding Cloudflare 1010.
DEFAULT_ERRORS_CN_URL = "https://sub2apicn.shujk.top/api/v1/admin/ops/errors"
DEFAULT_ERRORS_US_URL = "https://us.sub2.shujk.top/api/v1/admin/ops/errors"
DEFAULT_ERRORS_CN_URL_CONFIG_FILE = "~/.config/shusub2/errors-cn-url"
DEFAULT_ERRORS_US_URL_CONFIG_FILE = "~/.config/shusub2/errors-us-url"
DEFAULT_VERSION_CHECK_URL = "https://gitea.shujk.top/shujakuin/shusub2/raw/branch/main/pyproject.toml"
DEFAULT_REFRESH_SECONDS = 300
DEFAULT_LOGS_REFRESH_SECONDS = 300
DEFAULT_ERRORS_REFRESH_SECONDS = 300
DEFAULT_LOGS_LIMIT = 100
DEFAULT_ERRORS_LIMIT = 100
DEFAULT_ERRORS_TIME_RANGE = "24h"
DEFAULT_TIMEOUT_SECONDS = 10
DEFAULT_VERSION_CHECK_TIMEOUT_SECONDS = 2
MONITOR_OK_STATUSES = {"operational", "ok", "success"}
MONITOR_FAILED_STATUSES = {"error", "failed", "failure"}
MONITOR_STOPWORDS = {"response", "responses", "monitor"}
PRICING_ACCOUNT_SOURCE_RE = re.compile(
r"^[a-z0-9]+-quota(?:only)?-(?P<source>[a-z0-9]+(?:-[a-z0-9]+)*)$",
re.IGNORECASE,
)
INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/shusub2.git"
INSTALL_COMMAND_ARGS = ["uv", "tool", "install", "--force", "git+https://gitea.shujk.top/shujakuin/shusub2.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 default_api_url() -> str:
return configured_url(
("SHUSUB2_API_URL", "SUB2API_QUOTA_TUI_API_URL"),
os.environ.get("SHUSUB2_API_URL_FILE", DEFAULT_CONFIG_FILE),
DEFAULT_API_URL,
)
def default_status_url() -> str:
return configured_url(
("SHUSUB2_STATUS_URL",),
os.environ.get("SHUSUB2_STATUS_URL_FILE", DEFAULT_STATUS_CONFIG_FILE),
)
def default_pricing_url() -> str:
return configured_url(
("SHUSUB2_PRICING_URL",),
os.environ.get("SHUSUB2_PRICING_URL_FILE", DEFAULT_PRICING_URL_CONFIG_FILE),
DEFAULT_PRICING_URL,
)
def default_logs_url() -> str:
return configured_url(
("SHUSUB2_LOGS_URL",),
os.environ.get("SHUSUB2_LOGS_URL_FILE", DEFAULT_LOGS_URL_CONFIG_FILE),
DEFAULT_LOGS_API_URL,
)
def default_logs_token() -> str:
return configured_url(
("SHUSUB2_LOGS_TOKEN",),
os.environ.get("SHUSUB2_LOGS_TOKEN_FILE", DEFAULT_LOGS_TOKEN_CONFIG_FILE),
)
def config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_API_URL_FILE", DEFAULT_CONFIG_FILE)).expanduser()
def logs_url_config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_LOGS_URL_FILE", DEFAULT_LOGS_URL_CONFIG_FILE)).expanduser()
def pricing_url_config_file_path() -> Path:
return Path(
os.environ.get("SHUSUB2_PRICING_URL_FILE", DEFAULT_PRICING_URL_CONFIG_FILE)
).expanduser()
def logs_token_config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_LOGS_TOKEN_FILE", DEFAULT_LOGS_TOKEN_CONFIG_FILE)).expanduser()
def default_errors_cn_url() -> str:
return configured_url(
("SHUSUB2_ERRORS_CN_URL",),
os.environ.get("SHUSUB2_ERRORS_CN_URL_FILE", DEFAULT_ERRORS_CN_URL_CONFIG_FILE),
DEFAULT_ERRORS_CN_URL,
)
def default_errors_us_url() -> str:
return configured_url(
("SHUSUB2_ERRORS_US_URL",),
os.environ.get("SHUSUB2_ERRORS_US_URL_FILE", DEFAULT_ERRORS_US_URL_CONFIG_FILE),
DEFAULT_ERRORS_US_URL,
)
def errors_cn_url_config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_ERRORS_CN_URL_FILE", DEFAULT_ERRORS_CN_URL_CONFIG_FILE)).expanduser()
def errors_us_url_config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_ERRORS_US_URL_FILE", DEFAULT_ERRORS_US_URL_CONFIG_FILE)).expanduser()
def write_config_value(path: Path, value: str) -> Path:
text = str(value or "").strip()
if not text:
raise ValueError("config value is empty")
path.parent.mkdir(parents=True, exist_ok=True)
try:
path.parent.chmod(0o700)
except OSError:
pass
path.write_text(text + "\n", encoding="utf-8")
try:
path.chmod(0o600)
except OSError:
pass
return path
def write_api_url_config(api_url: str) -> Path:
value = str(api_url or "").strip()
if not value:
raise ValueError("api url is empty")
return write_config_value(config_file_path(), value)
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 inferred_status_url(api_url: str) -> str:
parsed = urllib.parse.urlparse(str(api_url or "").strip())
if not parsed.scheme or not parsed.netloc:
return ""
path = parsed.path.rstrip("/")
if path.endswith("/api/tui/accounts"):
status_path = path[: -len("/api/tui/accounts")] + "/api/status"
elif path.endswith("/api/accounts"):
status_path = path[: -len("/api/accounts")] + "/api/status"
else:
return ""
return urllib.parse.urlunparse(parsed._replace(path=status_path, params="", query="", fragment=""))
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("SHUSUB2_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 format_cost(value: Any) -> str:
amount = as_float(value)
if amount == 0:
return "$0"
if abs(amount) < 0.01:
return f"${amount:.6f}".rstrip("0").rstrip(".")
if abs(amount) < 10:
return f"${amount:.3f}".rstrip("0").rstrip(".")
return f"${amount:.2f}".rstrip("0").rstrip(".")
def optional_number(value: Any) -> float | None:
if value is None or str(value).strip() == "":
return None
try:
number = float(value)
except (TypeError, ValueError):
return None
return number if math.isfinite(number) else None
def format_amount(value: Any) -> str:
amount = optional_number(value)
if amount is None:
return "-"
if amount.is_integer():
return f"{int(amount):,}"
return f"{amount:,.6f}".rstrip("0").rstrip(".")
def format_cny(value: Any) -> str:
amount = format_amount(value)
return "-" if amount == "-" else f{amount}"
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 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 parse_time(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 short_time(value: Any) -> str:
parsed = parse_time(value)
if not parsed:
return "-"
return parsed.astimezone().strftime("%m-%d %H:%M")
def relative_age(value: Any, now: dt.datetime | None = None) -> str:
parsed = parse_time(value)
if not parsed:
return "-"
current = now or dt.datetime.now(dt.timezone.utc)
if current.tzinfo is None:
current = current.replace(tzinfo=dt.timezone.utc)
elapsed = (current.astimezone(dt.timezone.utc) - parsed.astimezone(dt.timezone.utc)).total_seconds()
if elapsed < -30:
return f"in {max(1, int((-elapsed) / 60))}m"
seconds = max(0, int(elapsed))
if seconds < 60:
return "now"
minutes = seconds // 60
if minutes < 60:
return f"{minutes}m ago"
hours = minutes // 60
if hours < 24:
return f"{hours}h ago"
return f"{hours // 24}d ago"
def add_refresh_param(api_url: str, refresh: bool) -> str:
if not refresh:
return api_url
parsed = urllib.parse.urlparse(api_url)
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
query = [(key, value) for key, value in query if key != "refresh"]
query.append(("refresh", "1"))
return urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query)))
def decode_json_response(response: Any, error_message: str) -> dict[str, Any]:
raw = response.read()
content_encoding = str(response.headers.get("Content-Encoding", "")).lower()
if "gzip" in {value.strip() for value in content_encoding.split(",")}:
raw = gzip.decompress(raw)
data = json.loads(raw.decode("utf-8"))
if not isinstance(data, dict):
raise RuntimeError(error_message)
return data
def fetch_payload(api_url: str, timeout: int, *, refresh: bool = False) -> dict[str, Any]:
req = urllib.request.Request(
add_refresh_param(api_url, refresh),
headers={"Accept": "application/json", "Accept-Encoding": "gzip"},
)
with urllib.request.urlopen(req, timeout=timeout) as response:
return decode_json_response(response, "API did not return a JSON object")
def fetch_optional_payload(url: str, timeout: int) -> tuple[dict[str, Any], str]:
if not str(url or "").strip():
return {}, ""
try:
return fetch_payload(url, timeout), ""
except Exception as exc:
return {}, str(exc)
def fetch_pricing_payload(pricing_url: str, timeout: int) -> dict[str, Any]:
payload = fetch_payload(pricing_url, timeout)
sources = payload.get("sources")
if payload.get("view") != "accounts" or not isinstance(sources, list):
raise RuntimeError("Pricing Monitor did not return an accounts source projection")
for source in sources:
if (
not isinstance(source, dict)
or not isinstance(source.get("name"), str)
or not source["name"].strip()
or not isinstance(source.get("source_kind"), str)
or not source["source_kind"].strip()
or not isinstance(source.get("health_state"), str)
or not source["health_state"].strip()
or not isinstance(source.get("balance_available"), bool)
or not isinstance(source.get("balance"), dict)
):
raise RuntimeError("Pricing Monitor returned an invalid accounts source projection")
return payload
def fetch_optional_pricing_payload(pricing_url: str, timeout: int) -> tuple[dict[str, Any], str]:
try:
return fetch_pricing_payload(pricing_url, timeout), ""
except Exception:
return {}, "unavailable"
def logs_request_url(logs_url: str, limit: int) -> str:
parsed = urllib.parse.urlparse(logs_url)
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
query = [(key, value) for key, value in query if key not in {"page", "page_size", "limit"}]
query.extend((("page", "1"), ("page_size", str(max(1, min(int(limit), 1000))))))
return urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query)))
def fetch_logs_payload(logs_url: str, token: str, timeout: int, limit: int = DEFAULT_LOGS_LIMIT) -> dict[str, Any]:
headers = {"Accept": "application/json", "Accept-Encoding": "gzip"}
if str(token or "").strip():
headers["x-api-key"] = str(token).strip()
req = urllib.request.Request(logs_request_url(logs_url, limit), headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as response:
return decode_json_response(response, "logs API did not return a JSON object")
def logs_envelope(payload: dict[str, Any]) -> dict[str, Any]:
data = payload.get("data")
if isinstance(data, dict) and isinstance(data.get("items"), list):
return data
return payload
def log_items(payload: dict[str, Any]) -> list[dict[str, Any]]:
items = logs_envelope(payload).get("items")
if not isinstance(items, list):
return []
return [item for item in items if isinstance(item, dict)]
def log_type_label(item: dict[str, Any]) -> str:
text = str(item.get("request_type") or "").strip().lower()
if text and text != "unknown":
return text
if item.get("openai_ws_mode"):
return "ws_v2"
if item.get("stream") is True:
return "stream"
if item.get("stream") is False:
return "sync"
return text or "-"
def log_cache_tokens(item: dict[str, Any]) -> int:
return as_int(item.get("cache_creation_tokens")) + as_int(item.get("cache_read_tokens"))
def log_total_tokens(item: dict[str, Any]) -> int:
return as_int(item.get("input_tokens")) + as_int(item.get("output_tokens")) + log_cache_tokens(item)
def log_tokens_per_second(item: dict[str, Any]) -> float:
output = as_int(item.get("output_tokens"))
duration = as_int(item.get("duration_ms"))
first = max(0, as_int(item.get("first_token_ms")))
decode_ms = duration - first
if output <= 0 or decode_ms <= 0:
return 0.0
return output * 1000.0 / decode_ms
def format_rate(value: Any) -> str:
rate = as_float(value)
if rate <= 0:
return "-"
if rate >= 100:
return f"{rate:.0f}/s"
return f"{rate:.1f}/s"
def format_seconds(value: Any) -> str:
number = as_int(value)
if number <= 0:
return "-"
return f"{number / 1000:.1f}s"
KEY_COLOR_PALETTE = (
"cyan",
"magenta",
"green",
"yellow",
"bright_blue",
"bright_cyan",
"bright_magenta",
"bright_green",
"bright_yellow",
"bright_red",
)
def key_color(name: Any) -> str:
text = str(name or "").strip()
if not text or text == "-":
return ""
return KEY_COLOR_PALETTE[zlib.crc32(text.encode("utf-8")) % len(KEY_COLOR_PALETTE)]
def admin_api_base(logs_url: str) -> str:
parsed = urllib.parse.urlparse(str(logs_url or "").strip())
if not parsed.scheme or not parsed.netloc:
return ""
path = parsed.path.rstrip("/")
if path.endswith("/usage"):
path = path[: -len("/usage")]
if not path.endswith("/admin"):
return ""
return urllib.parse.urlunparse(parsed._replace(path=path, params="", query="", fragment=""))
def fetch_admin_json(url: str, token: str, timeout: int, body: dict[str, Any] | None = None) -> dict[str, Any]:
headers = {"Accept": "application/json", "Accept-Encoding": "gzip"}
if str(token or "").strip():
headers["x-api-key"] = str(token).strip()
data = None
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as response:
return decode_json_response(response, "admin API did not return a JSON object")
def payload_data(payload: dict[str, Any]) -> dict[str, Any]:
data = payload.get("data")
return data if isinstance(data, dict) else payload
def fetch_key_usage_payload(logs_url: str, token: str, timeout: int, limit: int = 100) -> dict[str, Any]:
base = admin_api_base(logs_url)
if not base:
raise RuntimeError("cannot derive the admin API base from the logs url")
today = dt.date.today().isoformat()
query = urllib.parse.urlencode({"start_date": today, "end_date": today, "granularity": "day", "limit": limit})
trend_payload = fetch_admin_json(f"{base}/dashboard/api-keys-trend?{query}", token, timeout)
trend = payload_data(trend_payload).get("trend")
points = [point for point in trend if isinstance(point, dict)] if isinstance(trend, list) else []
ids = sorted({as_int(point.get("api_key_id")) for point in points if as_int(point.get("api_key_id"))})
stats: dict[str, Any] = {}
if ids:
costs_payload = fetch_admin_json(f"{base}/dashboard/api-keys-usage", token, timeout, body={"api_key_ids": ids})
raw_stats = payload_data(costs_payload).get("stats")
if isinstance(raw_stats, dict):
stats = raw_stats
return {"date": today, "trend": points, "stats": stats}
def normalize_key_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
needle = filter_text.strip().lower()
stats = payload.get("stats") if isinstance(payload.get("stats"), dict) else {}
merged: dict[Any, dict[str, Any]] = {}
for point in payload.get("trend") or []:
if not isinstance(point, dict):
continue
key_id = as_int(point.get("api_key_id"))
name = str(point.get("key_name") or "").strip() or (f"#{key_id}" if key_id else "-")
if needle and needle not in name.lower():
continue
row = merged.setdefault(key_id or name, {"id": key_id, "name": name, "requests": 0, "tokens": 0, "cost": 0.0})
row["requests"] += as_int(point.get("requests"))
row["tokens"] += as_int(point.get("tokens"))
for row in merged.values():
stat = stats.get(str(row["id"]))
if not isinstance(stat, dict):
stat = stats.get(row["id"])
if isinstance(stat, dict):
row["cost"] = as_float(stat.get("today_actual_cost"))
rows = list(merged.values())
rows.sort(key=lambda row: (-row["cost"], -row["tokens"], str(row["name"]).lower()))
return rows
def print_keys_once(payload: dict[str, Any]) -> None:
rows = normalize_key_rows(payload)
total_cost = sum(row["cost"] for row in rows)
print(f"keys today {payload.get('date') or '-'} | {len(rows)} keys | {format_cost(total_cost)}")
print("key today tokens req")
for row in rows:
print(
f"{str(row['name'])[:20]:<21} "
f"{format_cost(row['cost']):<10} "
f"{format_count(row['tokens']):<8} "
f"{format_count(row['requests'])}"
)
def nested_name(item: dict[str, Any], object_key: str, id_key: str, name_key: str = "") -> str:
nested = item.get(object_key)
if isinstance(nested, dict):
name = str(nested.get("name") or "").strip()
if name:
return name
if name_key:
name = str(item.get(name_key) or "").strip()
if name:
return name
identifier = as_int(item.get(id_key))
return f"#{identifier}" if identifier else "-"
def normalize_log_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
needle = filter_text.strip().lower()
rows = []
for item in log_items(payload):
key_name = nested_name(item, "api_key", "api_key_id", "api_key_name")
account_name = nested_name(item, "account", "account_id", "account_name")
user_name = nested_name(item, "user", "user_id", "user_name")
model = str(item.get("model") or "-")
upstream_model = str(item.get("upstream_model") or "").strip()
effort = str(item.get("reasoning_effort") or "").strip() or "-"
type_label = log_type_label(item)
row = {
"id": as_int(item.get("id")),
"key": key_name,
"account": account_name,
"user": user_name,
"model": model,
"upstream_model": upstream_model,
"effort": effort,
"type": type_label,
"tokens": log_total_tokens(item),
"input_tokens": as_int(item.get("input_tokens")),
"output_tokens": as_int(item.get("output_tokens")),
"cache_creation_tokens": as_int(item.get("cache_creation_tokens")),
"cache_read_tokens": as_int(item.get("cache_read_tokens")),
"cache_tokens": log_cache_tokens(item),
"cost": as_float(item.get("total_cost")),
"actual_cost": as_float(item.get("actual_cost")),
"duration_ms": as_int(item.get("duration_ms")),
"first_token_ms": as_int(item.get("first_token_ms")),
"tokens_per_second": log_tokens_per_second(item),
"created_at": str(item.get("created_at") or ""),
"time": short_time(item.get("created_at")),
"age": relative_age(item.get("created_at")),
"request_id": str(item.get("request_id") or ""),
"raw": item,
}
if needle:
haystack = " ".join(
(key_name, account_name, user_name, model, upstream_model, effort, type_label, row["request_id"])
).lower()
if needle not in haystack:
continue
rows.append(row)
rows.sort(key=lambda row: (row["created_at"], row["id"]), reverse=True)
return rows
def logs_summary_line(payload: dict[str, Any], shown: int) -> str:
envelope = logs_envelope(payload)
total = as_int(envelope.get("total"))
page_size = as_int(envelope.get("page_size"))
fetched = short_time(dt.datetime.now(dt.timezone.utc).isoformat())
cost = sum(as_float(item.get("total_cost")) for item in log_items(payload))
tokens = sum(log_total_tokens(item) for item in log_items(payload))
return (
f"{fetched} logs | showing {shown}/{len(log_items(payload))} of latest {page_size or '-'} | "
f"total {format_count(total)} records | page cost {format_cost(cost)} | {format_count(tokens)} tokens"
)
def log_detail_line(row: dict[str, Any]) -> str:
model = row["model"]
if row["upstream_model"] and row["upstream_model"] != row["model"]:
model = f"{row['model']} -> {row['upstream_model']}"
if row["effort"] != "-":
model = f"{model} ({row['effort']})"
detail = (
f"{row['time']} | key {row['key']} | account {row['account']} | user {row['user']} | {model} | {row['type']} | "
f"tokens in {format_count(row['input_tokens'])} out {format_count(row['output_tokens'])} "
f"cache_w {format_count(row['cache_creation_tokens'])} cache_r {format_count(row['cache_read_tokens'])} | "
f"cost {format_cost(row['cost'])} (actual {format_cost(row['actual_cost'])}) | "
f"first {format_seconds(row['first_token_ms'])} duration {format_seconds(row['duration_ms'])} "
f"{format_rate(row['tokens_per_second'])}"
)
if row["request_id"]:
detail += f" | {row['request_id']}"
return detail
def print_logs_once(payload: dict[str, Any], filter_text: str = "") -> None:
rows = normalize_log_rows(payload, filter_text)
print(logs_summary_line(payload, len(rows)))
print("key account model effort type input output cache tokens cost first duration tok/s time age")
for row in rows:
print(
f"{row['key'][:20]:<21} "
f"{row['account'][:20]:<21} "
f"{row['model'][:24]:<25} "
f"{row['effort']:<7} "
f"{row['type']:<7} "
f"{format_count(row['input_tokens']):<8} "
f"{format_count(row['output_tokens']):<8} "
f"{format_count(row['cache_tokens']):<8} "
f"{format_count(row['tokens']):<8} "
f"{format_cost(row['cost']):<10} "
f"{format_seconds(row['first_token_ms']):<8} "
f"{format_seconds(row['duration_ms']):<10} "
f"{format_rate(row['tokens_per_second']):<8} "
f"{row['time']:<11} "
f"{row['age']}"
)
def errors_request_url(errors_url: str, limit: int, time_range: str = DEFAULT_ERRORS_TIME_RANGE) -> str:
parsed = urllib.parse.urlparse(errors_url)
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
query = [(key, value) for key, value in query if key not in {"page", "page_size", "limit", "time_range"}]
page_size = max(1, min(int(limit), 500))
query.extend(
(
("page", "1"),
("page_size", str(page_size)),
("time_range", str(time_range or DEFAULT_ERRORS_TIME_RANGE).strip() or DEFAULT_ERRORS_TIME_RANGE),
)
)
return urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query)))
def fetch_errors_payload(
errors_url: str,
token: str,
timeout: int,
limit: int = DEFAULT_ERRORS_LIMIT,
time_range: str = DEFAULT_ERRORS_TIME_RANGE,
) -> dict[str, Any]:
headers = {"Accept": "application/json", "Accept-Encoding": "gzip"}
if str(token or "").strip():
headers["x-api-key"] = str(token).strip()
req = urllib.request.Request(errors_request_url(errors_url, limit, time_range), headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as response:
return decode_json_response(response, "errors API did not return a JSON object")
def error_items(payload: dict[str, Any]) -> list[dict[str, Any]]:
data = payload.get("data")
if isinstance(data, dict) and isinstance(data.get("items"), list):
items = data.get("items")
elif isinstance(payload.get("items"), list):
items = payload.get("items")
else:
items = []
return [item for item in items if isinstance(item, dict)]
def error_total(payload: dict[str, Any]) -> int:
data = payload.get("data")
if isinstance(data, dict) and data.get("total") is not None:
return as_int(data.get("total"))
if payload.get("total") is not None:
return as_int(payload.get("total"))
return len(error_items(payload))
def default_error_sources(cn_url: str, us_url: str) -> list[tuple[str, str]]:
sources: list[tuple[str, str]] = []
if str(cn_url or "").strip():
sources.append(("cn", str(cn_url).strip()))
if str(us_url or "").strip():
sources.append(("us", str(us_url).strip()))
return sources
def fetch_merged_errors(
sources: list[tuple[str, str]],
token: str,
timeout: int,
limit: int = DEFAULT_ERRORS_LIMIT,
time_range: str = DEFAULT_ERRORS_TIME_RANGE,
) -> dict[str, Any]:
source_status: dict[str, dict[str, Any]] = {}
merged_items: list[dict[str, Any]] = []
def _one(node: str, url: str) -> tuple[str, str, dict[str, Any] | None, str]:
try:
return node, url, fetch_errors_payload(url, token, timeout, limit, time_range), ""
except Exception as exc:
return node, url, None, str(exc)
if not sources:
return {"items": [], "sources": {}, "time_range": time_range, "limit": limit}
with ThreadPoolExecutor(max_workers=max(1, len(sources))) as pool:
futures = [pool.submit(_one, node, url) for node, url in sources]
for future in as_completed(futures):
node, url, payload, error = future.result()
if payload is None:
source_status[node] = {"ok": False, "url": url, "total": 0, "fetched": 0, "error": error}
continue
items = error_items(payload)
for item in items:
row = dict(item)
row["_node"] = node
row["_source_url"] = url
merged_items.append(row)
source_status[node] = {
"ok": True,
"url": url,
"total": error_total(payload),
"fetched": len(items),
"error": "",
}
merged_items.sort(
key=lambda item: (str(item.get("created_at") or ""), as_int(item.get("id")), str(item.get("_node") or "")),
reverse=True,
)
return {
"items": merged_items,
"sources": source_status,
"time_range": time_range,
"limit": limit,
"generated_at": dt.datetime.now(dt.timezone.utc).isoformat(),
}
def normalize_error_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
needle = filter_text.strip().lower()
rows = []
for item in payload.get("items") or []:
if not isinstance(item, dict):
continue
node = str(item.get("_node") or item.get("node") or "-").strip() or "-"
key_name = str(item.get("api_key_name") or "").strip()
if not key_name and item.get("api_key_id") is not None:
key_name = f"#{as_int(item.get('api_key_id'))}"
if not key_name:
key_name = "-"
account_name = str(item.get("account_name") or "").strip()
if not account_name and item.get("account_id") is not None:
account_name = f"#{as_int(item.get('account_id'))}"
if not account_name:
account_name = "-"
user_email = str(item.get("user_email") or "").strip()
if not user_email and item.get("user_id") is not None:
user_email = f"#{as_int(item.get('user_id'))}"
model = str(item.get("requested_model") or item.get("model") or "-").strip() or "-"
upstream_model = str(item.get("upstream_model") or "").strip()
phase = str(item.get("phase") or "-").strip() or "-"
error_type = str(item.get("type") or "-").strip() or "-"
owner = str(item.get("error_owner") or "-").strip() or "-"
source = str(item.get("error_source") or "-").strip() or "-"
platform = str(item.get("platform") or "-").strip() or "-"
status_code = as_int(item.get("status_code"))
message = str(item.get("message") or "").strip()
request_id = str(item.get("request_id") or "").strip()
client_request_id = str(item.get("client_request_id") or "").strip()
group_name = str(item.get("group_name") or "").strip() or "-"
row = {
"id": as_int(item.get("id")),
"node": node,
"status_code": status_code,
"key": key_name,
"account": account_name,
"user": user_email or "-",
"model": model,
"upstream_model": upstream_model,
"phase": phase,
"type": error_type,
"owner": owner,
"source": source,
"platform": platform,
"group": group_name,
"message": message,
"request_id": request_id,
"client_request_id": client_request_id,
"created_at": str(item.get("created_at") or ""),
"time": short_time(item.get("created_at")),
"age": relative_age(item.get("created_at")),
"resolved": bool(item.get("resolved")),
"raw": item,
}
if needle:
haystack = " ".join(
(
node,
key_name,
account_name,
user_email,
model,
upstream_model,
phase,
error_type,
owner,
source,
platform,
group_name,
message,
request_id,
client_request_id,
str(status_code),
)
).lower()
if needle not in haystack:
continue
rows.append(row)
rows.sort(key=lambda row: (row["created_at"], row["id"], row["node"]), reverse=True)
return rows
def errors_summary_line(payload: dict[str, Any], shown: int) -> str:
sources = payload.get("sources") if isinstance(payload.get("sources"), dict) else {}
source_bits = []
for node in sorted(sources.keys()):
info = sources.get(node) if isinstance(sources.get(node), dict) else {}
if info.get("ok"):
source_bits.append(f"{node} {as_int(info.get('fetched'))}/{as_int(info.get('total'))}")
else:
source_bits.append(f"{node} err")
fetched = short_time(payload.get("generated_at") or dt.datetime.now(dt.timezone.utc).isoformat())
time_range = str(payload.get("time_range") or DEFAULT_ERRORS_TIME_RANGE)
source_text = ", ".join(source_bits) if source_bits else "no sources"
return f"{fetched} errors | showing {shown}/{len(payload.get('items') or [])} | {time_range} | {source_text}"
def error_detail_line(row: dict[str, Any]) -> str:
model = row["model"]
if row["upstream_model"] and row["upstream_model"] != row["model"]:
model = f"{row['model']} -> {row['upstream_model']}"
message = row["message"] or "-"
if len(message) > 180:
message = message[:177] + "..."
detail = (
f"{row['time']} | {row['node']} | status {row['status_code']} | key {row['key']} | account {row['account']} | "
f"user {row['user']} | {model} | {row['platform']} | phase {row['phase']} | type {row['type']} | "
f"owner {row['owner']} | source {row['source']} | group {row['group']} | {message}"
)
if row["request_id"]:
detail += f" | {row['request_id']}"
if row["client_request_id"] and row["client_request_id"] != row["request_id"]:
detail += f" | client {row['client_request_id']}"
if row["resolved"]:
detail += " | resolved"
return detail
def print_errors_once(payload: dict[str, Any], filter_text: str = "") -> None:
rows = normalize_error_rows(payload, filter_text)
print(errors_summary_line(payload, len(rows)))
sources = payload.get("sources") if isinstance(payload.get("sources"), dict) else {}
for node in sorted(sources.keys()):
info = sources.get(node) if isinstance(sources.get(node), dict) else {}
if info.get("ok"):
print(f"{node}: ok fetched {as_int(info.get('fetched'))} total {as_int(info.get('total'))} | {info.get('url') or '-'}")
else:
print(f"{node}: error {info.get('error') or 'unknown'} | {info.get('url') or '-'}")
print("node status key account model phase type owner time age")
for row in rows:
print(
f"{row['node']:<5} "
f"{row['status_code']:<7} "
f"{row['key'][:20]:<21} "
f"{row['account'][:20]:<21} "
f"{row['model'][:24]:<25} "
f"{row['phase'][:9]:<10} "
f"{row['type'][:13]:<14} "
f"{row['owner'][:8]:<9} "
f"{row['time']:<11} "
f"{row['age']}"
)
def kind_label(kind: Any) -> str:
return {
"quota_limited": "quota",
"pending_quota": "pending",
"daily_limited": "daily",
"pay_as_you_go": "usage",
"disabled": "disabled",
}.get(str(kind or ""), str(kind or "unknown"))
def provider_label(account: dict[str, Any]) -> str:
text = str(account.get("provider") or account.get("platform") or "").strip().lower()
if not text:
return "-"
if "anthropic" in text or "claude" in text:
return "anthropic"
if "openai" in text or "chatgpt" in text:
return "openai"
return text
def routing_group_sort_rank(value: Any) -> int:
return {"sfast": 0, "fast": 1, "slow": 2, "id": 3, "": 4, "-": 4}.get(str(value or "").strip().lower(), 4)
def status_kind(value: Any) -> str:
text = str(value or "").strip().lower()
if text in MONITOR_OK_STATUSES:
return "ok"
if text in MONITOR_FAILED_STATUSES:
return "failed"
return "unknown"
def format_latency(value: Any) -> str:
number = as_int(value)
return "-" if number <= 0 else f"{number}ms"
def monitor_items(status_payload: dict[str, Any]) -> list[dict[str, Any]]:
monitors = status_payload.get("channel_monitors") if isinstance(status_payload.get("channel_monitors"), dict) else {}
items = []
for item in monitors.get("items") or []:
if isinstance(item, dict):
items.append(item)
return items
def monitor_summary(status_payload: dict[str, Any], error: str = "") -> str:
if error:
return f"monitors error: {error}"
monitors = status_payload.get("channel_monitors") if isinstance(status_payload.get("channel_monitors"), dict) else {}
if not monitors:
return "monitors -"
enabled = as_int(monitors.get("enabled"))
ok = as_int(monitors.get("latest_ok"))
failed = as_int(monitors.get("latest_failed"))
unknown = as_int(monitors.get("latest_unknown"))
checked = short_time(monitors.get("latest_checked_max"))
return f"monitors {ok} ok / {failed} failed / {unknown} unknown ({enabled} enabled, {checked})"
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 token not in MONITOR_STOPWORDS and not token.isdigit():
tokens.add(token)
return tokens
def matching_monitor(row: dict[str, Any], status_payload: dict[str, Any]) -> dict[str, Any] | None:
base_url_hash = str(row.get("base_url_hash") or "").strip()
if base_url_hash:
for item in monitor_items(status_payload):
if str(item.get("base_url_hash") or "").strip() == base_url_hash:
return item
account_tokens = text_tokens(row.get("name"))
if not account_tokens:
return None
best: tuple[int, dict[str, Any]] | None = None
for item in monitor_items(status_payload):
monitor_tokens = text_tokens(item.get("name"))
if not monitor_tokens:
continue
overlap = account_tokens & monitor_tokens
if not overlap:
continue
subset_bonus = 10 if account_tokens <= monitor_tokens or monitor_tokens <= account_tokens else 0
score = subset_bonus + len(overlap)
if best is None or score > best[0]:
best = (score, item)
return None if best is None else best[1]
def monitor_detail(row: dict[str, Any], status_payload: dict[str, Any]) -> str:
item = matching_monitor(row, status_payload)
if not item:
return "monitor -"
status = str(item.get("latest_status") or "unknown")
latency = format_latency(item.get("latency_ms"))
checked = short_time(item.get("checked_at"))
message = str(item.get("message") or "").strip()
detail = f"monitor {status} {latency} checked {checked}"
if message:
detail += f" {message}"
return detail
def monitor_availability(row: dict[str, Any], status_payload: dict[str, Any]) -> str:
item = matching_monitor(row, status_payload)
if not item:
return "-"
if item.get("enabled") is False:
return "disabled"
raw_status = str(item.get("latest_status") or "").strip().lower()
if not raw_status:
return "unknown"
kind = status_kind(raw_status)
if kind == "ok":
return "ok"
if kind == "failed":
return "failed"
return raw_status
def window_for(account: dict[str, Any], window_id: str) -> dict[str, Any]:
for window in account.get("windows") or []:
if isinstance(window, dict) and window.get("id") == window_id:
return window
return {}
def window_cell(account: dict[str, Any], window_id: str) -> str:
window = window_for(account, window_id)
if not window or window.get("used_percent") is None:
return "-"
return f"{format_percent(window.get('used_percent'))}/{format_percent(window.get('remaining_percent'))}"
def daily_quota_cell(account: dict[str, Any]) -> str:
limit = account.get("daily_quota_limit")
used = account.get("daily_quota_used")
if limit is None:
return "-"
return f"{used or 0}/{limit}"
def reset_cell(account: dict[str, Any]) -> str:
daily_reset = short_time(account.get("daily_quota_reset_at"))
if daily_reset != "-":
return daily_reset
five_hour = window_for(account, "five-hour")
weekly = window_for(account, "weekly")
for window in (five_hour, weekly):
reset = short_time(window.get("reset"))
if reset != "-":
return reset
return "-"
def normalize_account_rows(
payload: dict[str, Any],
filter_text: str = "",
pricing_payload: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
needle = filter_text.strip().lower()
pricing_sources = pricing_source_lookup(pricing_payload or {})
rows = []
for account in payload.get("accounts") or []:
if not isinstance(account, dict):
continue
pricing_source = pricing_source_for_account(account.get("name"), pricing_sources)
haystack = " ".join(
str(account.get(key) or "")
for key in (
"id",
"name",
"routing_group",
"provider",
"kind",
"status",
"account_type",
"platform",
"plan",
"daily_quota_timezone",
)
).lower()
if pricing_source:
haystack = f"{haystack} {pricing_source.get('name') or ''} {pricing_source.get('status') or ''}".lower()
if needle and needle not in haystack:
continue
routing_group = str(account.get("routing_group") or "-")
rows.append(
{
"id": as_int(account.get("id")),
"name": str(account.get("name") or ""),
"routing_group": routing_group,
"base_url_hash": str(account.get("base_url_hash") or ""),
"pricing_source": str(pricing_source.get("name") or "-") if pricing_source else "-",
"pricing_cny": pricing_source.get("balance_cny") if pricing_source and pricing_source.get("status") == "healthy" else None,
"pricing_state": str(pricing_source.get("status") or "-") if pricing_source else "-",
"pricing_balance": pricing_source.get("balance") if pricing_source else None,
"pricing_unit": str(pricing_source.get("unit") or "-") if pricing_source else "-",
"pricing_updated": str(pricing_source.get("updated") or "-") if pricing_source else "-",
"provider": provider_label(account),
"kind": str(account.get("kind") or "unknown"),
"kind_label": kind_label(account.get("kind")),
"status": str(account.get("status") or ""),
"account_type": str(account.get("account_type") or ""),
"plan": str(account.get("plan") or ""),
"priority": account.get("priority"),
"today_cost_usd": as_float(account.get("today_cost_usd")),
"today_actual_cost_usd": as_float(account.get("today_actual_cost_usd")),
"today_tokens": as_int(account.get("today_tokens")),
"today_requests": as_int(account.get("today_requests")),
"daily_quota": account.get("daily_quota") if isinstance(account.get("daily_quota"), dict) else {},
"daily_quota_limit": account.get("daily_quota_limit"),
"daily_quota_used": account.get("daily_quota_used"),
"daily_quota_remaining": account.get("daily_quota_remaining"),
"daily_quota_used_percent": account.get("daily_quota_used_percent"),
"daily_quota_reset_at": account.get("daily_quota_reset_at"),
"daily_quota_cell": daily_quota_cell(account),
"quota_used_percent_max": account.get("quota_used_percent_max"),
"five_hour": window_cell(account, "five-hour"),
"weekly": window_cell(account, "weekly"),
"reset": reset_cell(account),
"latest_usage_at": short_time(account.get("latest_usage_at") or account.get("last_used_at")),
"error": str(account.get("error") or ""),
"raw": account,
}
)
rows.sort(
key=lambda item: (
routing_group_sort_rank(item["routing_group"]),
1 if item["kind"] == "disabled" else 0,
-as_float(item["today_cost_usd"]),
-as_int(item["today_tokens"]),
-as_float(item["daily_quota_used_percent"]),
-as_float(item["quota_used_percent_max"]),
str(item["name"]).lower(),
)
)
return rows
def pricing_status(source: dict[str, Any]) -> str:
if str(source.get("last_error") or "").strip():
return "error"
state = str(source.get("health_state") or "").strip().lower()
return state or ("refreshing" if source.get("refreshing") else "unknown")
def normalize_pricing_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
needle = filter_text.strip().lower()
rows: list[dict[str, Any]] = []
for source in payload.get("sources") or []:
if not isinstance(source, dict):
continue
balance = source.get("balance") if isinstance(source.get("balance"), dict) else {}
balance_is_available = source.get("balance_available") is not False
row = {
"name": str(source.get("name") or "-"),
"kind": str(source.get("source_kind") or "-"),
"balance": optional_number(balance.get("available")) if balance_is_available else None,
"balance_cny": optional_number(balance.get("available_cny")) if balance_is_available else None,
"unit": str(balance.get("unit") or "-"),
"status": pricing_status(source),
"updated_at": source.get("last_success_at"),
"updated": short_time(source.get("last_success_at")),
"age": relative_age(source.get("last_success_at")),
"error": str(source.get("last_error") or "").strip(),
"raw": source,
}
haystack = " ".join(
str(row[key] or "") for key in ("name", "kind", "unit", "status", "error")
).lower()
if needle and needle not in haystack:
continue
rows.append(row)
rows.sort(key=lambda row: (row["status"] != "healthy", row["name"].lower()))
return rows
def pricing_source_key(value: Any) -> str:
return re.sub(r"[^a-z0-9]+", "", str(value or "").strip().casefold())
def pricing_source_lookup(payload: dict[str, Any]) -> dict[str, dict[str, Any] | None]:
lookup: dict[str, dict[str, Any] | None] = {}
for source in normalize_pricing_rows(payload):
key = pricing_source_key(source.get("name"))
if not key:
continue
if key in lookup:
lookup[key] = None
else:
lookup[key] = source
return lookup
def pricing_source_for_account(
account_name: Any, pricing_sources: dict[str, dict[str, Any] | None]
) -> dict[str, Any] | None:
match = PRICING_ACCOUNT_SOURCE_RE.fullmatch(str(account_name or "").strip())
if not match:
return None
return pricing_sources.get(pricing_source_key(match.group("source")))
def pricing_summary(payload: dict[str, Any], rows: list[dict[str, Any]] | None = None) -> str:
source_rows = rows if rows is not None else normalize_pricing_rows(payload)
healthy = sum(row["status"] == "healthy" for row in source_rows)
cny_values = [
row["balance_cny"]
for row in source_rows
if row["status"] == "healthy" and row["balance_cny"] is not None
]
total_cny = sum(cny_values) if cny_values else None
total_label = f" | CNY {format_cny(total_cny)}" if total_cny is not None else ""
return f"upstreams {healthy}/{len(source_rows)} healthy{total_label}"
def pricing_detail_line(row: dict[str, Any]) -> str:
detail = (
f"{row['name']} | {row['kind']} | balance {format_amount(row['balance'])} {row['unit']} | "
f"CNY {format_cny(row['balance_cny'])} | {row['status']} | "
f"updated {row['updated']} ({row['age']})"
)
if row["error"]:
detail += f" | {row['error']}"
return detail
def account_pricing_detail(row: dict[str, Any]) -> str:
source = str(row.get("pricing_source") or "-")
if source == "-":
return "upstream -"
cny = format_cny(row.get("pricing_cny"))
raw_balance = format_amount(row.get("pricing_balance"))
unit = str(row.get("pricing_unit") or "-")
state = str(row.get("pricing_state") or "-")
updated = str(row.get("pricing_updated") or "-")
return f"upstream {source} | CNY {cny} | raw {raw_balance} {unit} | {state} | updated {updated}"
def print_pricing_once(payload: dict[str, Any], filter_text: str = "") -> None:
rows = normalize_pricing_rows(payload, filter_text)
print(f"{short_time(payload.get('generated_at'))} | {pricing_summary(payload, rows)}")
print("source kind balance cny unit status updated age")
for row in rows:
print(
f"{row['name']:<30} "
f"{row['kind']:<15} "
f"{format_amount(row['balance']):<19} "
f"{format_cny(row['balance_cny']):<19} "
f"{row['unit']:<9} "
f"{row['status']:<11} "
f"{row['updated']:<12} "
f"{row['age']}"
)
def summary_line(payload: dict[str, Any]) -> str:
totals = payload.get("totals") if isinstance(payload.get("totals"), dict) else {}
generated = short_time(payload.get("generated_local") or payload.get("generated_at"))
cached = "cached" if payload.get("cached") else "live"
return (
f"{generated} {cached} | "
f"{as_int(totals.get('usable_accounts'))}/{as_int(totals.get('total_accounts'))} usable | "
f"today {format_cost(totals.get('today_cost_usd'))} | "
f"{format_count(totals.get('today_tokens'))} tokens | "
f"{format_count(totals.get('today_requests'))} req"
)
def print_once(
payload: dict[str, Any],
filter_text: str = "",
status_payload: dict[str, Any] | None = None,
status_error: str = "",
pricing_payload: dict[str, Any] | None = None,
pricing_error: str = "",
) -> None:
print(summary_line(payload))
if status_payload or status_error:
print(monitor_summary(status_payload or {}, status_error))
if pricing_error:
print(f"upstreams: {pricing_error}")
elif pricing_payload:
print(pricing_summary(pricing_payload))
print("name provider group source src cny src state daily today tokens req kind 5h 7d reset status availability")
for row in normalize_account_rows(payload, filter_text, pricing_payload):
print(
f"{row['name'][:30]:<30} "
f"{row['provider']:<9} "
f"{row['routing_group']:<6} "
f"{row['pricing_source']:<16} "
f"{format_cny(row['pricing_cny']):<13} "
f"{row['pricing_state']:<12} "
f"{row['daily_quota_cell']:<10} "
f"{format_cost(row['today_cost_usd']):<10} "
f"{format_count(row['today_tokens']):<8} "
f"{format_count(row['today_requests']):<5} "
f"{row['kind_label']:<9} "
f"{row['five_hour']:<9} "
f"{row['weekly']:<9} "
f"{row['reset']:<11} "
f"{row['status']:<10} "
f"{monitor_availability(row, status_payload or {})}"
)
def run_textual(
api_url: str,
status_url: str,
pricing_url: str,
logs_url: str,
logs_token: str,
errors_cn_url: str,
errors_us_url: str,
refresh_seconds: int,
logs_refresh_seconds: int,
errors_refresh_seconds: int,
timeout: int,
logs_limit: int,
errors_limit: int,
errors_time_range: str,
version_message: str = "",
start_page: str = "dashboard",
) -> int:
try:
from rich.text import Text
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import DataTable, Footer, Header, Input, Static
except ImportError:
print("Textual is required. Run with: uv run --with textual python sub2api_quota_tui.py", file=sys.stderr)
return 2
class DashboardScreen(Screen[None]):
AUTO_FOCUS = "#accounts"
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "focus_accounts", "Accounts"),
("p", "show_pricing", "Upstreams"),
("k", "focus_keys", "Keys"),
("l", "focus_logs", "Logs"),
("e", "focus_errors", "Errors"),
]
def __init__(self) -> None:
super().__init__(classes="dashboard")
self.accounts_payload: dict[str, Any] = {}
self.status_payload: dict[str, Any] = {}
self.pricing_payload: dict[str, Any] = {}
self.pricing_error = ""
self.monitor_error = ""
self.accounts_error = ""
self.logs_payload: dict[str, Any] = {}
self.logs_error = ""
self.errors_payload: dict[str, Any] = {}
self.errors_error = ""
self.keys_payload: dict[str, Any] = {}
self.keys_error = ""
self.account_rows: list[dict[str, Any]] = []
self.key_rows: list[dict[str, Any]] = []
self.log_rows: list[dict[str, Any]] = []
self.error_rows: list[dict[str, Any]] = []
self.account_by_key: dict[str, dict[str, Any]] = {}
self.key_by_key: dict[str, dict[str, Any]] = {}
self.log_by_key: dict[str, dict[str, Any]] = {}
self.error_by_key: dict[str, dict[str, Any]] = {}
self.active_table = "accounts"
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield Static("", id="summary")
yield Input(placeholder="filter", id="filter")
yield DataTable(id="accounts")
yield DataTable(id="keys")
yield DataTable(id="logs")
yield DataTable(id="errors")
yield Static("", id="detail")
yield Static("", id="status")
yield Footer()
@staticmethod
def configure_table(table: DataTable, columns: tuple[str, ...]) -> None:
table.cursor_type = "row"
table.zebra_stripes = True
# Content-sized columns preserve complete values on wide terminals;
# narrow terminals use DataTable's horizontal scrolling instead.
for label in columns:
table.add_column(label)
def on_mount(self) -> None:
self.configure_table(
self.query_one("#accounts", DataTable),
("ACCOUNT", "Group", "Source", "Src CNY", "Src state", "Today", "Daily", "5h", "7d", "Avail"),
)
self.configure_table(
self.query_one("#keys", DataTable),
("KEY", "Today", "Tokens", "Req"),
)
self.configure_table(
self.query_one("#logs", DataTable),
(
"LOG KEY",
"Account",
"Model",
"First",
"Duration",
"Tok/s",
"Input",
"Output",
"Cache",
"Tokens",
"Cost",
"Time",
"Age",
),
)
self.configure_table(
self.query_one("#errors", DataTable),
("ERR", "Status", "Key", "Account", "Model", "Time", "Age"),
)
self.refresh_all(refresh=True)
self.set_interval(refresh_seconds, self.refresh_accounts)
self.set_interval(refresh_seconds, self.refresh_pricing)
self.set_interval(logs_refresh_seconds, self.refresh_logs)
self.set_interval(errors_refresh_seconds, self.refresh_errors)
self.focus_table("accounts")
def action_refresh(self) -> None:
self.refresh_all(refresh=True)
def action_focus_filter(self) -> None:
self.query_one("#filter", Input).focus()
def action_focus_accounts(self) -> None:
self.focus_table("accounts")
def action_show_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_focus_keys(self) -> None:
table = self.query_one("#keys", DataTable)
if table.display:
self.focus_table("keys")
def action_focus_logs(self) -> None:
self.focus_table("logs")
def action_focus_errors(self) -> None:
self.focus_table("errors")
def focus_table(self, table_id: str) -> None:
self.active_table = table_id
self.query_one(f"#{table_id}", DataTable).focus()
self.render_active_detail()
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "filter":
self.render_tables()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "filter":
self.focus_table(self.active_table)
def refresh_all(self, refresh: bool = False) -> None:
self.query_one("#status", Static).update("refreshing accounts, upstream balances, keys, logs, and errors...")
self.refresh_accounts(refresh=refresh)
self.refresh_pricing()
self.refresh_logs()
self.refresh_errors()
def refresh_accounts(self, refresh: bool = False) -> None:
self.accounts_error = ""
try:
self.accounts_payload = fetch_payload(api_url, timeout, refresh=refresh)
self.status_payload, self.monitor_error = fetch_optional_payload(status_url, timeout)
except Exception as exc:
self.accounts_error = str(exc)
self.render_accounts()
self.render_meta()
def refresh_pricing(self) -> None:
self.pricing_payload, self.pricing_error = fetch_optional_pricing_payload(
pricing_url, timeout
)
self.render_accounts()
self.render_meta()
def refresh_logs(self) -> None:
self.logs_error = ""
if not str(logs_token or "").strip():
self.logs_payload = {}
self.logs_error = "admin token not configured"
else:
try:
self.logs_payload = fetch_logs_payload(logs_url, logs_token, timeout, logs_limit)
except Exception as exc:
self.logs_error = str(exc)
self.render_logs()
self.refresh_keys()
self.render_meta()
def refresh_keys(self) -> None:
self.keys_error = ""
if not str(logs_token or "").strip():
self.keys_payload = {}
self.keys_error = "admin token not configured"
else:
try:
self.keys_payload = fetch_key_usage_payload(logs_url, logs_token, timeout)
except Exception as exc:
self.keys_payload = {}
self.keys_error = str(exc)
self.render_keys()
def refresh_errors(self) -> None:
self.errors_error = ""
if not str(logs_token or "").strip():
self.errors_payload = {}
self.errors_error = "admin token not configured"
else:
try:
self.errors_payload = fetch_merged_errors(
default_error_sources(errors_cn_url, errors_us_url),
logs_token,
timeout,
errors_limit,
errors_time_range,
)
except Exception as exc:
self.errors_error = str(exc)
self.render_errors()
self.render_meta()
def render_tables(self) -> None:
self.render_accounts()
self.render_keys()
self.render_logs()
self.render_errors()
self.render_meta()
self.render_active_detail()
def render_accounts(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.account_rows = normalize_account_rows(
self.accounts_payload, filter_text, self.pricing_payload
)
table = self.query_one("#accounts", DataTable)
table.clear()
self.account_by_key = {}
for index, row in enumerate(self.account_rows):
key = f"account-{row['id']}-{index}"
self.account_by_key[key] = row
table.add_row(
row["name"],
row["routing_group"],
row["pricing_source"],
format_cny(row["pricing_cny"]),
row["pricing_state"],
format_cost(row["today_cost_usd"]),
row["daily_quota_cell"],
row["five_hour"],
row["weekly"],
monitor_availability(row, self.status_payload),
key=key,
)
def render_keys(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.key_rows = normalize_key_rows(self.keys_payload, filter_text)
table = self.query_one("#keys", DataTable)
table.clear()
self.key_by_key = {}
for index, row in enumerate(self.key_rows):
key = f"key-{row['id']}-{index}"
self.key_by_key[key] = row
color = key_color(row["name"])
table.add_row(
Text(str(row["name"]), style=color) if color else str(row["name"]),
format_cost(row["cost"]),
format_count(row["tokens"]),
format_count(row["requests"]),
key=key,
)
table.display = bool(self.key_rows) and self.app.size.height >= 20
def render_logs(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.log_rows = normalize_log_rows(self.logs_payload, filter_text)
table = self.query_one("#logs", DataTable)
table.clear()
self.log_by_key = {}
for index, row in enumerate(self.log_rows):
key = f"log-{row['id']}-{index}"
self.log_by_key[key] = row
color = key_color(row["key"])
table.add_row(
Text(row["key"], style=color) if color else row["key"],
row["account"],
row["model"],
format_seconds(row["first_token_ms"]),
format_seconds(row["duration_ms"]),
format_rate(row["tokens_per_second"]),
format_count(row["input_tokens"]),
format_count(row["output_tokens"]),
format_count(row["cache_tokens"]),
format_count(row["tokens"]),
format_cost(row["cost"]),
row["time"],
row["age"],
key=key,
)
def render_errors(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.error_rows = normalize_error_rows(self.errors_payload, filter_text)
table = self.query_one("#errors", DataTable)
table.clear()
self.error_by_key = {}
for index, row in enumerate(self.error_rows):
key = f"error-{row['node']}-{row['id']}-{index}"
self.error_by_key[key] = row
table.add_row(
row["node"],
str(row["status_code"]),
row["key"],
row["account"],
row["model"],
row["time"],
row["age"],
key=key,
)
def errors_total(self) -> int:
sources = self.errors_payload.get("sources") if isinstance(self.errors_payload.get("sources"), dict) else {}
totals = [
as_int(info.get("total"))
for info in sources.values()
if isinstance(info, dict) and info.get("ok")
]
return sum(totals) if totals else len(self.errors_payload.get("items") or [])
def render_meta(self) -> None:
totals = self.accounts_payload.get("totals") if isinstance(self.accounts_payload.get("totals"), dict) else {}
account_total = as_int(totals.get("total_accounts")) or len(self.accounts_payload.get("accounts") or [])
usable = as_int(totals.get("usable_accounts"))
logs_total = as_int(logs_envelope(self.logs_payload).get("total"))
if not logs_total:
logs_total = len(log_items(self.logs_payload))
key_summary = "keys -"
if self.key_rows:
top_key = self.key_rows[0]
key_summary = f"keys {top_key['name']} {format_cost(top_key['cost'])}"
summary = Text()
summary.append(
f"accounts {len(self.account_rows)}/{account_total} ({usable} usable, {format_cost(totals.get('today_cost_usd'))})",
style="green",
)
summary.append(" | ", style="dim")
if self.pricing_error:
summary.append("upstreams unavailable", style="bright_blue")
else:
summary.append(pricing_summary(self.pricing_payload), style="bright_blue")
summary.append(" | ", style="dim")
summary.append(f"keys {len(self.key_rows)} ({key_summary.removeprefix('keys ')})", style="yellow")
summary.append(" | ", style="dim")
summary.append(f"logs {len(self.log_rows)}/{logs_total}", style="magenta")
summary.append(" | ", style="dim")
summary.append(f"errors {len(self.error_rows)}/{self.errors_total()}", style="red")
self.query_one("#summary", Static).update(summary)
status_bits = [version_message]
if self.accounts_error:
status_bits.append(f"accounts: {self.accounts_error}")
else:
status_bits.extend(
(
monitor_summary(self.status_payload, self.monitor_error),
f"source {self.accounts_payload.get('source_name') or '-'}",
)
)
if self.pricing_error:
status_bits.append(f"upstreams: {self.pricing_error}")
elif self.pricing_payload:
status_bits.append(pricing_summary(self.pricing_payload) + f" | {pricing_url}")
shared_admin_error = self.logs_error and self.logs_error == self.errors_error
if shared_admin_error:
status_bits.append(f"logs/errors: {self.logs_error}")
else:
if self.logs_error:
status_bits.append(f"logs: {self.logs_error}")
if self.errors_error:
status_bits.append(f"errors: {self.errors_error}")
if self.keys_error and self.keys_error != self.logs_error:
status_bits.append(f"keys: {self.keys_error}")
if not self.errors_error and self.errors_payload:
sources = self.errors_payload.get("sources") if isinstance(self.errors_payload.get("sources"), dict) else {}
source_bits = []
for node in ("cn", "us"):
info = sources.get(node) if isinstance(sources.get(node), dict) else {}
if info:
source_bits.append(f"{node} {'ok' if info.get('ok') else 'err'}")
if source_bits:
status_bits.append("errors " + "/".join(source_bits))
self.query_one("#status", Static).update(" | ".join(bit for bit in status_bits if bit) or "ready")
def render_active_detail(self) -> None:
rows_by_table = {
"accounts": self.account_rows,
"keys": self.key_rows,
"logs": self.log_rows,
"errors": self.error_rows,
}
rows = rows_by_table[self.active_table]
if not rows:
self.query_one("#detail", Static).update(f"no {self.active_table}")
return
self.render_detail(self.active_table, rows[0])
def render_detail(self, table_id: str, row: dict[str, Any]) -> None:
if table_id == "accounts":
detail = (
f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | "
f"daily {row['daily_quota_cell']} | "
f"5h {row['five_hour']} | "
f"7d {row['weekly']} | "
f"today {format_cost(row['today_cost_usd'])}, "
f"{format_count(row['today_tokens'])} tokens, {format_count(row['today_requests'])} req | "
f"{account_pricing_detail(row)} | "
f"{monitor_detail(row, self.status_payload)}"
)
error = str(row.get("error") or "").strip()
if error:
detail += f" | {error}"
elif table_id == "keys":
detail = (
f"{row['name']} | today {format_cost(row['cost'])} | "
f"{format_count(row['tokens'])} tokens | {format_count(row['requests'])} req"
)
elif table_id == "logs":
detail = log_detail_line(row)
else:
detail = error_detail_line(row)
self.query_one("#detail", Static).update(detail)
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
table_id = str(event.control.id or "")
if table_id not in {"accounts", "keys", "logs", "errors"}:
return
if event.control.has_focus:
self.active_table = table_id
if table_id != self.active_table:
return
row_maps = {
"accounts": self.account_by_key,
"keys": self.key_by_key,
"logs": self.log_by_key,
"errors": self.error_by_key,
}
row = row_maps[table_id].get(str(event.row_key.value))
if row:
self.render_detail(table_id, row)
class AccountsScreen(Screen[None]):
AUTO_FOCUS = "#accounts"
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("p", "show_pricing", "Upstreams"),
("l", "show_logs", "Logs"),
("e", "show_errors", "Errors"),
]
def __init__(self) -> None:
super().__init__()
self.payload: dict[str, Any] = {}
self.status_payload: dict[str, Any] = {}
self.pricing_payload: dict[str, Any] = {}
self.pricing_error = ""
self.status_error = ""
self.keys_payload: dict[str, Any] = {}
self.keys_error = ""
self.rows: list[dict[str, Any]] = []
self.row_by_key: dict[str, dict[str, Any]] = {}
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield Static("", id="summary")
yield Input(placeholder="filter", id="filter")
yield DataTable(id="accounts")
yield DataTable(id="keys")
yield Static("", id="detail")
yield Static("", id="status")
yield Footer()
def on_mount(self) -> None:
table = self.query_one("#accounts", DataTable)
table.cursor_type = "row"
table.zebra_stripes = True
table.add_columns("Name", "Provider", "Group", "Source", "Src CNY", "Src state", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status", "Availability")
keys_table = self.query_one("#keys", DataTable)
keys_table.cursor_type = "row"
keys_table.zebra_stripes = True
keys_table.add_columns("Key", "Today", "Tokens", "Req")
self.refresh_data(refresh=True)
self.set_interval(refresh_seconds, self.refresh_data)
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_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_show_logs(self) -> None:
self.app.switch_screen(LogsScreen())
def action_show_errors(self) -> None:
self.app.switch_screen(ErrorsScreen())
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "filter":
self.render_payload()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "filter":
self.query_one("#accounts", DataTable).focus()
def refresh_data(self, refresh: bool = False) -> None:
status = self.query_one("#status", Static)
status.update("refreshing...")
try:
self.payload = fetch_payload(api_url, timeout, refresh=refresh)
self.status_payload, self.status_error = fetch_optional_payload(status_url, timeout)
self.pricing_payload, self.pricing_error = fetch_optional_pricing_payload(
pricing_url, timeout
)
self.refresh_keys()
self.render_payload()
self.render_keys()
status_bits = [version_message, monitor_summary(self.status_payload, self.status_error), f"source {self.payload.get('source_name') or '-'}"]
if self.pricing_error:
status_bits.append(f"upstreams: {self.pricing_error}")
else:
status_bits.append(pricing_summary(self.pricing_payload))
if self.keys_error:
status_bits.append(f"keys: {self.keys_error}")
status.update(" | ".join(bit for bit in status_bits if bit) + f" | {api_url}")
except Exception as exc:
status.update(f"error: {exc}")
def refresh_keys(self) -> None:
self.keys_error = ""
self.keys_payload = {}
if not str(logs_token or "").strip():
self.keys_error = "logs token not configured"
return
try:
self.keys_payload = fetch_key_usage_payload(logs_url, logs_token, timeout)
except Exception as exc:
self.keys_error = str(exc)
def render_keys(self) -> None:
table = self.query_one("#keys", DataTable)
table.clear()
rows = normalize_key_rows(self.keys_payload)
table.display = bool(rows)
for row in rows:
color = key_color(row["name"])
table.add_row(
Text(str(row["name"]), style=color) if color else str(row["name"]),
format_cost(row["cost"]),
format_count(row["tokens"]),
format_count(row["requests"]),
)
def render_payload(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.rows = normalize_account_rows(self.payload, filter_text, self.pricing_payload)
table = self.query_one("#accounts", DataTable)
table.clear()
self.row_by_key = {}
for row in self.rows:
key = str(row["id"])
self.row_by_key[key] = row
table.add_row(
row["name"],
row["provider"],
row["routing_group"],
row["pricing_source"],
format_cny(row["pricing_cny"]),
row["pricing_state"],
row["daily_quota_cell"],
format_cost(row["today_cost_usd"]),
format_count(row["today_tokens"]),
format_count(row["today_requests"]),
row["kind_label"],
row["five_hour"],
row["weekly"],
row["reset"],
row["status"],
monitor_availability(row, self.status_payload),
key=key,
)
self.query_one("#summary", Static).update(summary_line(self.payload))
if self.rows:
self.render_detail(self.rows[0])
else:
self.query_one("#detail", Static).update("no accounts")
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
key = str(event.row_key.value)
row = self.row_by_key.get(key)
if row:
self.render_detail(row)
def render_detail(self, row: dict[str, Any]) -> None:
raw = row.get("raw") if isinstance(row.get("raw"), dict) else {}
detail = (
f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | {row['account_type']} | "
f"daily {row['daily_quota_cell']} ({format_percent(row['daily_quota_used_percent'])} used, "
f"{row['daily_quota_remaining'] if row['daily_quota_remaining'] is not None else '-'} left) | "
f"5h {row['five_hour']} | 7d {row['weekly']} | "
f"{account_pricing_detail(row)} | "
f"today {format_cost(row['today_cost_usd'])}, {format_count(row['today_tokens'])} tokens, "
f"{format_count(row['today_requests'])} req | latest {row['latest_usage_at']} | "
f"priority {row['priority'] if row['priority'] is not None else '-'} | "
f"{monitor_detail(row, self.status_payload)}"
)
error = str(raw.get("error") or "").strip()
if error:
detail += f" | {error}"
self.query_one("#detail", Static).update(detail)
class PricingScreen(Screen[None]):
AUTO_FOCUS = "#pricing"
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "show_accounts", "Accounts"),
("l", "show_logs", "Logs"),
("e", "show_errors", "Errors"),
]
def __init__(self) -> None:
super().__init__()
self.payload: dict[str, Any] = {}
self.rows: list[dict[str, Any]] = []
self.row_by_key: dict[str, dict[str, Any]] = {}
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield Static("", id="summary")
yield Input(placeholder="filter upstream sources", id="filter")
yield DataTable(id="pricing")
yield Static("", id="detail")
yield Static("", id="status")
yield Footer()
def on_mount(self) -> None:
table = self.query_one("#pricing", DataTable)
table.cursor_type = "row"
table.zebra_stripes = True
table.add_columns("Source", "Kind", "Balance", "CNY", "Unit", "Status", "Updated", "Age")
self.refresh_data()
self.set_interval(refresh_seconds, self.refresh_data)
def action_refresh(self) -> None:
self.refresh_data()
def action_focus_filter(self) -> None:
self.query_one("#filter", Input).focus()
def action_show_accounts(self) -> None:
self.app.switch_screen(AccountsScreen())
def action_show_logs(self) -> None:
self.app.switch_screen(LogsScreen())
def action_show_errors(self) -> None:
self.app.switch_screen(ErrorsScreen())
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "filter":
self.render_payload()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "filter":
self.query_one("#pricing", DataTable).focus()
def refresh_data(self) -> None:
status = self.query_one("#status", Static)
status.update("refreshing upstream balances from Pricing Monitor...")
try:
self.payload = fetch_pricing_payload(pricing_url, timeout)
self.render_payload()
status_bits = [version_message, pricing_summary(self.payload), pricing_url]
status.update(" | ".join(bit for bit in status_bits if bit))
except Exception:
self.payload = {}
self.rows = []
self.row_by_key = {}
self.query_one("#pricing", DataTable).clear()
self.query_one("#summary", Static).update("upstreams unavailable")
self.query_one("#detail", Static).update("no upstream balances")
status.update("upstreams unavailable")
def render_payload(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.rows = normalize_pricing_rows(self.payload, filter_text)
table = self.query_one("#pricing", DataTable)
table.clear()
self.row_by_key = {}
for index, row in enumerate(self.rows):
key = f"pricing-{row['name']}-{index}"
self.row_by_key[key] = row
table.add_row(
row["name"],
row["kind"],
format_amount(row["balance"]),
format_cny(row["balance_cny"]),
row["unit"],
row["status"],
row["updated"],
row["age"],
key=key,
)
self.query_one("#summary", Static).update(pricing_summary(self.payload, self.rows))
if self.rows:
self.query_one("#detail", Static).update(pricing_detail_line(self.rows[0]))
else:
self.query_one("#detail", Static).update("no upstream balances")
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
row = self.row_by_key.get(str(event.row_key.value))
if row:
self.query_one("#detail", Static).update(pricing_detail_line(row))
class LogsScreen(Screen[None]):
AUTO_FOCUS = "#logs"
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "show_accounts", "Accounts"),
("p", "show_pricing", "Upstreams"),
("e", "show_errors", "Errors"),
]
def __init__(self) -> None:
super().__init__()
self.payload: dict[str, Any] = {}
self.rows: list[dict[str, Any]] = []
self.row_by_key: dict[str, dict[str, Any]] = {}
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield Static("", id="summary")
yield Input(placeholder="filter", id="filter")
yield DataTable(id="logs")
yield Static("", id="detail")
yield Static("", id="status")
yield Footer()
def on_mount(self) -> None:
table = self.query_one("#logs", DataTable)
table.cursor_type = "row"
table.zebra_stripes = True
table.add_columns(
"Key",
"Account",
"Model",
"Effort",
"Type",
"Input",
"Output",
"Cache",
"Tokens",
"Cost",
"First",
"Duration",
"Tok/s",
"Time",
"Age",
)
self.refresh_data()
self.set_interval(logs_refresh_seconds, self.refresh_data)
def action_refresh(self) -> None:
self.refresh_data()
def action_focus_filter(self) -> None:
self.query_one("#filter", Input).focus()
def action_show_accounts(self) -> None:
self.app.switch_screen(AccountsScreen())
def action_show_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_show_errors(self) -> None:
self.app.switch_screen(ErrorsScreen())
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "filter":
self.render_payload()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "filter":
self.query_one("#logs", DataTable).focus()
def refresh_data(self) -> None:
status = self.query_one("#status", Static)
if not str(logs_token or "").strip():
status.update(logs_token_hint())
return
status.update("refreshing logs...")
try:
self.payload = fetch_logs_payload(logs_url, logs_token, timeout, logs_limit)
self.render_payload()
status_bits = [version_message, f"latest {logs_limit} requests | {logs_url}"]
status.update(" | ".join(bit for bit in status_bits if bit))
except Exception as exc:
status.update(f"logs error: {exc}")
def render_payload(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.rows = normalize_log_rows(self.payload, filter_text)
table = self.query_one("#logs", DataTable)
table.clear()
self.row_by_key = {}
for index, row in enumerate(self.rows):
key = f"{row['id']}-{index}"
self.row_by_key[key] = row
color = key_color(row["key"])
table.add_row(
Text(row["key"], style=color) if color else row["key"],
row["account"],
row["model"],
row["effort"],
row["type"],
format_count(row["input_tokens"]),
format_count(row["output_tokens"]),
format_count(row["cache_tokens"]),
format_count(row["tokens"]),
format_cost(row["cost"]),
format_seconds(row["first_token_ms"]),
format_seconds(row["duration_ms"]),
format_rate(row["tokens_per_second"]),
row["time"],
row["age"],
key=key,
)
self.query_one("#summary", Static).update(logs_summary_line(self.payload, len(self.rows)))
if self.rows:
self.query_one("#detail", Static).update(log_detail_line(self.rows[0]))
else:
self.query_one("#detail", Static).update("no request logs")
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
key = str(event.row_key.value)
row = self.row_by_key.get(key)
if row:
self.query_one("#detail", Static).update(log_detail_line(row))
class ErrorsScreen(Screen[None]):
AUTO_FOCUS = "#errors"
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "show_accounts", "Accounts"),
("p", "show_pricing", "Upstreams"),
("l", "show_logs", "Logs"),
]
def __init__(self) -> None:
super().__init__()
self.payload: dict[str, Any] = {}
self.rows: list[dict[str, Any]] = []
self.row_by_key: dict[str, dict[str, Any]] = {}
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield Static("", id="summary")
yield Input(placeholder="filter", id="filter")
yield DataTable(id="errors")
yield Static("", id="detail")
yield Static("", id="status")
yield Footer()
def on_mount(self) -> None:
table = self.query_one("#errors", DataTable)
table.cursor_type = "row"
table.zebra_stripes = True
table.add_columns("Node", "Status", "Key", "Account", "Model", "Phase", "Type", "Owner", "Time", "Age")
self.refresh_data()
self.set_interval(errors_refresh_seconds, self.refresh_data)
def action_refresh(self) -> None:
self.refresh_data()
def action_focus_filter(self) -> None:
self.query_one("#filter", Input).focus()
def action_show_accounts(self) -> None:
self.app.switch_screen(AccountsScreen())
def action_show_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_show_logs(self) -> None:
self.app.switch_screen(LogsScreen())
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "filter":
self.render_payload()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "filter":
self.query_one("#errors", DataTable).focus()
def refresh_data(self) -> None:
status = self.query_one("#status", Static)
if not str(logs_token or "").strip():
status.update(logs_token_hint())
return
status.update("refreshing errors from cn + us...")
try:
self.payload = fetch_merged_errors(
default_error_sources(errors_cn_url, errors_us_url),
logs_token,
timeout,
errors_limit,
errors_time_range,
)
self.render_payload()
sources = self.payload.get("sources") if isinstance(self.payload.get("sources"), dict) else {}
bits = []
for node in ("cn", "us"):
info = sources.get(node) if isinstance(sources.get(node), dict) else {}
if not info:
continue
if info.get("ok"):
bits.append(f"{node} ok")
else:
bits.append(f"{node} {info.get('error') or 'error'}")
status_bits = [version_message, f"every {errors_refresh_seconds}s", " | ".join(bits)]
status.update(" | ".join(bit for bit in status_bits if bit))
except Exception as exc:
status.update(f"errors error: {exc}")
def render_payload(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.rows = normalize_error_rows(self.payload, filter_text)
table = self.query_one("#errors", DataTable)
table.clear()
self.row_by_key = {}
for index, row in enumerate(self.rows):
key = f"{row['node']}-{row['id']}-{index}"
self.row_by_key[key] = row
table.add_row(
row["node"],
str(row["status_code"]),
row["key"],
row["account"],
row["model"],
row["phase"],
row["type"],
row["owner"],
row["time"],
row["age"],
key=key,
)
self.query_one("#summary", Static).update(errors_summary_line(self.payload, len(self.rows)))
if self.rows:
self.query_one("#detail", Static).update(error_detail_line(self.rows[0]))
else:
self.query_one("#detail", Static).update("no error logs")
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
key = str(event.row_key.value)
row = self.row_by_key.get(key)
if row:
self.query_one("#detail", Static).update(error_detail_line(row))
class Sub2APIQuotaApp(App[None]):
CSS = """
#summary { height: 1; padding: 0 1; color: $foreground; }
#filter { height: 1; border: none; padding: 0 1; }
#filter:focus { border: none; }
#accounts { height: 1fr; min-height: 3; }
#pricing { height: 1fr; min-height: 3; }
#keys { height: auto; max-height: 6; }
.dashboard #accounts { background: $success 5%; }
.dashboard #accounts > .datatable--header { background: $success 35%; color: $foreground; }
.dashboard #keys { height: 1fr; min-height: 0; max-height: 100%; background: $warning 5%; }
.dashboard #keys > .datatable--header { background: $warning 35%; color: $foreground; }
#logs { height: 1fr; min-height: 3; }
.dashboard #logs { background: $secondary 5%; }
.dashboard #logs > .datatable--header { background: $secondary 35%; color: $foreground; }
#errors { height: 1fr; min-height: 3; }
.dashboard #errors { background: $error 5%; }
.dashboard #errors > .datatable--header { background: $error 35%; color: $foreground; }
#detail { height: 2; padding: 0 1; background: $surface-lighten-1; }
#status { height: 1; padding: 0 1; color: $text-muted; }
"""
BINDINGS = [
("q", "quit", "Quit"),
]
def on_mount(self) -> None:
if start_page == "accounts":
self.push_screen(AccountsScreen())
elif start_page == "pricing":
self.push_screen(PricingScreen())
elif start_page == "logs":
self.push_screen(LogsScreen())
elif start_page == "errors":
self.push_screen(ErrorsScreen())
else:
self.push_screen(DashboardScreen())
Sub2APIQuotaApp().run()
return 0
def logs_token_hint() -> str:
return (
"logs token not configured: set SHUSUB2_LOGS_TOKEN or write the Sub2API admin API key to "
f"{logs_token_config_file_path()}"
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Sub2API quota and daily usage TUI")
parser.add_argument("--api-url", default=default_api_url())
parser.add_argument("--status-url", default=default_status_url(), help="optional sub2api-status /api/status URL for channel monitor health")
parser.add_argument("--pricing-url", default=default_pricing_url(), help="Pricing Monitor /api/ui-data?view=accounts URL for upstream source balances")
parser.add_argument("--logs-url", default=default_logs_url(), help="Sub2API admin usage logs URL (default: sub2apicn /api/v1/admin/usage)")
parser.add_argument("--logs-token", default=default_logs_token(), help="Sub2API admin API key for logs/errors pages (prefer SHUSUB2_LOGS_TOKEN or ~/.config/shusub2/logs-token)")
parser.add_argument("--errors-cn-url", default=default_errors_cn_url(), help="cn Sub2API admin ops errors URL (default: sub2apicn /api/v1/admin/ops/errors)")
parser.add_argument(
"--errors-us-url",
default=default_errors_us_url(),
help="us Sub2API admin ops errors URL (default: DNS-only us.sub2 /api/v1/admin/ops/errors; CF sub2apius often blocks non-browser clients)",
)
parser.add_argument("--save-config", action="store_true", help="persist --api-url/--pricing-url/--logs-url/--logs-token/--errors-*-url to ~/.config/shusub2/ before running")
parser.add_argument("--install", action="store_true", help="persist config, install shusub2 as a uv tool, then exit")
parser.add_argument("--version-check-url", default=os.environ.get("SHUSUB2_VERSION_CHECK_URL", DEFAULT_VERSION_CHECK_URL))
parser.add_argument(
"--version-check-timeout",
type=int,
default=env_int("SHUSUB2_VERSION_CHECK_TIMEOUT", DEFAULT_VERSION_CHECK_TIMEOUT_SECONDS),
)
parser.add_argument("--no-version-check", action="store_true", default=os.environ.get("SHUSUB2_NO_VERSION_CHECK", "").strip().lower() in {"1", "true", "yes", "on"})
parser.add_argument(
"--refresh-seconds",
type=int,
default=env_int("SUB2API_QUOTA_TUI_REFRESH_SECONDS", DEFAULT_REFRESH_SECONDS),
)
parser.add_argument(
"--logs-refresh-seconds",
type=int,
default=env_int("SHUSUB2_LOGS_REFRESH_SECONDS", DEFAULT_LOGS_REFRESH_SECONDS),
)
parser.add_argument(
"--errors-refresh-seconds",
type=int,
default=env_int("SHUSUB2_ERRORS_REFRESH_SECONDS", DEFAULT_ERRORS_REFRESH_SECONDS),
)
parser.add_argument(
"--logs-limit",
type=int,
default=env_int("SHUSUB2_LOGS_LIMIT", DEFAULT_LOGS_LIMIT),
)
parser.add_argument(
"--errors-limit",
type=int,
default=env_int("SHUSUB2_ERRORS_LIMIT", DEFAULT_ERRORS_LIMIT),
)
parser.add_argument(
"--errors-time-range",
default=os.environ.get("SHUSUB2_ERRORS_TIME_RANGE", DEFAULT_ERRORS_TIME_RANGE),
help="ops errors time_range query (5m/30m/1h/6h/24h/7d/30d, default 24h)",
)
parser.add_argument("--timeout", type=int, default=env_int("SUB2API_QUOTA_TUI_TIMEOUT", DEFAULT_TIMEOUT_SECONDS))
parser.add_argument("--once", action="store_true", help="print one snapshot and exit")
parser.add_argument("--accounts", action="store_true", help="start on the dedicated accounts page (with --once: print the accounts snapshot)")
parser.add_argument("--pricing", action="store_true", help="start on the Pricing Monitor upstream balances page (with --once: print source balances)")
parser.add_argument("--logs", action="store_true", help="start on the dedicated request logs page (with --once: print a logs snapshot)")
parser.add_argument("--errors", action="store_true", help="start on the dedicated merged cn+us errors page (with --once: print an errors snapshot)")
parser.add_argument("--filter", default="", help="initial filter for --once output")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
status_url = str(args.status_url or "").strip() or inferred_status_url(args.api_url)
logs_limit = max(1, min(args.logs_limit, 1000))
errors_limit = max(1, min(args.errors_limit, 500))
errors_time_range = str(args.errors_time_range or DEFAULT_ERRORS_TIME_RANGE).strip() or DEFAULT_ERRORS_TIME_RANGE
selected_pages = sum(bool(value) for value in (args.accounts, args.pricing, args.logs, args.errors))
if selected_pages > 1:
print("choose only one of --accounts, --pricing, --logs, or --errors", file=sys.stderr)
return 2
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 str(args.pricing_url or "").strip():
pricing_url_path = write_config_value(pricing_url_config_file_path(), args.pricing_url)
print(f"saved pricing url to {pricing_url_path}")
if str(args.logs_url or "").strip():
logs_url_path = write_config_value(logs_url_config_file_path(), args.logs_url)
print(f"saved logs url to {logs_url_path}")
if str(args.logs_token or "").strip():
logs_token_path = write_config_value(logs_token_config_file_path(), args.logs_token)
print(f"saved logs token to {logs_token_path}")
if str(args.errors_cn_url or "").strip():
errors_cn_path = write_config_value(errors_cn_url_config_file_path(), args.errors_cn_url)
print(f"saved errors cn url to {errors_cn_path}")
if str(args.errors_us_url or "").strip():
errors_us_path = write_config_value(errors_us_url_config_file_path(), args.errors_us_url)
print(f"saved errors us url to {errors_us_path}")
if args.install:
print("installing shusub2 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)
if args.pricing:
try:
print_pricing_once(
fetch_pricing_payload(args.pricing_url, args.timeout), args.filter
)
except Exception:
print("upstreams unavailable", file=sys.stderr)
return 1
return 0
if args.logs:
if not str(args.logs_token or "").strip():
print(logs_token_hint(), file=sys.stderr)
return 2
print_logs_once(fetch_logs_payload(args.logs_url, args.logs_token, args.timeout, logs_limit), args.filter)
return 0
if args.errors:
if not str(args.logs_token or "").strip():
print(logs_token_hint(), file=sys.stderr)
return 2
print_errors_once(
fetch_merged_errors(
default_error_sources(args.errors_cn_url, args.errors_us_url),
args.logs_token,
args.timeout,
errors_limit,
errors_time_range,
),
args.filter,
)
return 0
status_payload, status_error = fetch_optional_payload(status_url, args.timeout)
pricing_payload, pricing_error = fetch_optional_pricing_payload(args.pricing_url, args.timeout)
print_once(
fetch_payload(args.api_url, args.timeout, refresh=True),
args.filter,
status_payload,
status_error,
pricing_payload,
pricing_error,
)
if str(args.logs_token or "").strip():
print()
try:
print_keys_once(fetch_key_usage_payload(args.logs_url, args.logs_token, args.timeout))
except Exception as exc:
print(f"keys error: {exc}", file=sys.stderr)
return 0
start_page = "dashboard"
if args.accounts:
start_page = "accounts"
elif args.pricing:
start_page = "pricing"
elif args.logs:
start_page = "logs"
elif args.errors:
start_page = "errors"
return run_textual(
args.api_url,
status_url,
args.pricing_url,
args.logs_url,
args.logs_token,
args.errors_cn_url,
args.errors_us_url,
max(1, args.refresh_seconds),
max(1, args.logs_refresh_seconds),
max(1, args.errors_refresh_seconds),
max(1, args.timeout),
logs_limit,
errors_limit,
errors_time_range,
version_message,
start_page=start_page,
)
if __name__ == "__main__":
raise SystemExit(main())