294 lines
11 KiB
Python

"""Read-only provider-pool status derived from Switchyard and local auth metadata."""
from __future__ import annotations
import base64
import json
import os
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
SWITCHYARD_ROOT = os.environ.get(
"HERMES_SWITCHYARD_STATUS_URL",
"http://hermes-switchyard.hermes.svc.cluster.local:9005",
).rstrip("/")
CODEX_USAGE_URL = "https://chatgpt.com/codex/settings/usage"
CLAUDE_USAGE_URL = "https://claude.ai/settings/usage"
CODEX_AUTH_PATH = Path(
os.environ.get("CODEX_HOME", "/opt/data/home/.codex")
) / "auth.json"
CLAUDE_AUTH_PATH = Path(
os.environ.get("CLAUDE_CONFIG_DIR", "/opt/data/home/.claude")
) / ".credentials.json"
def _read_json(path: Path) -> dict[str, Any]:
"""Return one local JSON object without leaking parsing details."""
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, TypeError, ValueError, json.JSONDecodeError):
return {}
return value if isinstance(value, dict) else {}
def _get_json(url: str, timeout: float = 3.0) -> dict[str, Any]:
"""Fetch a bounded JSON status response from the in-cluster router."""
request = Request(url, headers={"Accept": "application/json"})
try:
with urlopen(request, timeout=timeout) as response:
value = json.load(response)
except (HTTPError, URLError, OSError, TimeoutError, ValueError):
return {}
return value if isinstance(value, dict) else {}
def _jwt_claims(token: Any) -> dict[str, Any]:
"""Decode trusted local token metadata for display; do not verify or return it."""
if not isinstance(token, str):
return {}
try:
encoded = token.split(".")[1]
encoded += "=" * (-len(encoded) % 4)
value = json.loads(base64.urlsafe_b64decode(encoded))
except (
IndexError,
TypeError,
ValueError,
json.JSONDecodeError,
base64.binascii.Error,
):
return {}
return value if isinstance(value, dict) else {}
def _timestamp(value: Any) -> tuple[str | None, bool | None]:
"""Normalize an epoch or ISO-8601 timestamp and report whether it is live."""
if isinstance(value, str):
rendered_value = value.strip()
if rendered_value and not rendered_value.replace(".", "", 1).isdigit():
try:
parsed = datetime.fromisoformat(rendered_value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
except ValueError:
return None, None
rendered = parsed.astimezone(timezone.utc).isoformat()
return rendered, parsed.timestamp() > time.time()
try:
raw = float(value)
except (TypeError, ValueError):
return None, None
if raw > 100_000_000_000:
raw /= 1000
try:
rendered = datetime.fromtimestamp(raw, tz=timezone.utc).isoformat()
except (OverflowError, OSError, ValueError):
return None, None
return rendered, raw > time.time()
def _codex_account() -> dict[str, Any]:
"""Return non-secret Codex plan and credential health metadata."""
auth = _read_json(CODEX_AUTH_PATH)
tokens = auth.get("tokens") if isinstance(auth.get("tokens"), dict) else {}
access = _jwt_claims(tokens.get("access_token"))
identity = _jwt_claims(tokens.get("id_token"))
claims = identity.get("https://api.openai.com/auth")
claims = claims if isinstance(claims, dict) else {}
expires_at, token_live = _timestamp(access.get("exp"))
subscription_until, subscription_live = _timestamp(
claims.get("chatgpt_subscription_active_until")
)
authenticated = bool(tokens.get("access_token")) and token_live is not False
return {
"authenticated": authenticated,
"auth_mode": auth.get("auth_mode") or "unknown",
"plan": claims.get("chatgpt_plan_type") or "unknown",
"token_expires_at": expires_at,
"subscription_active": subscription_live,
"subscription_until": subscription_until,
"last_refresh": auth.get("last_refresh"),
"quota_reported": False,
"usage_url": CODEX_USAGE_URL,
}
def _claude_account() -> dict[str, Any]:
"""Return non-secret Claude plan and credential health metadata."""
auth = _read_json(CLAUDE_AUTH_PATH)
oauth = auth.get("claudeAiOauth")
oauth = oauth if isinstance(oauth, dict) else {}
expires_at, token_live = _timestamp(oauth.get("expiresAt"))
refresh_expires_at, refresh_live = _timestamp(oauth.get("refreshTokenExpiresAt"))
refreshable = bool(oauth.get("refreshToken")) and refresh_live is not False
authenticated = (
bool(oauth.get("accessToken")) and token_live is not False
) or refreshable
return {
"authenticated": authenticated,
"access_token_live": token_live,
"refreshable": refreshable,
"plan": oauth.get("subscriptionType") or "unknown",
"rate_limit_tier": oauth.get("rateLimitTier") or "unknown",
"token_expires_at": expires_at,
"refresh_expires_at": refresh_expires_at,
"refresh_live": refresh_live,
"quota_reported": False,
"usage_url": CLAUDE_USAGE_URL,
}
def _provider_for_model(model_id: str) -> str:
"""Map Switchyard target identifiers to their operator-facing provider."""
value = model_id.lower()
if "/codex/" in value or value.startswith("openai"):
return "codex"
if "/claude/" in value or value.startswith("anthropic"):
return "claude"
if "/local/" in value or "qwen" in value or "ollama" in value:
return "local"
return "other"
def _number(value: Any) -> int:
"""Coerce non-negative counters from Switchyard's JSON safely."""
try:
return max(0, int(value or 0))
except (TypeError, ValueError):
return 0
def _provider_summary(name: str, models: dict[str, Any]) -> dict[str, Any]:
"""Aggregate successful calls, errors, tokens, and latency for one lane."""
selected: list[dict[str, Any]] = []
totals = {
"calls": 0,
"errors": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"reasoning_tokens": 0,
"total_tokens": 0,
}
latency_weight = 0.0
latency_calls = 0
for model_id, raw in sorted(models.items()):
if _provider_for_model(str(model_id)) != name or not isinstance(raw, dict):
continue
item = {"id": str(model_id)}
for key in totals:
item[key] = _number(raw.get(key))
totals[key] += item[key]
try:
item["avg_latency_ms"] = round(float(raw.get("avg_latency_ms") or 0), 1)
except (TypeError, ValueError):
item["avg_latency_ms"] = 0.0
latency_weight += item["avg_latency_ms"] * item["calls"]
latency_calls += item["calls"]
selected.append(item)
calls = totals["calls"]
errors = totals["errors"]
if calls and errors:
state = "degraded"
elif calls:
state = "available"
elif errors:
state = "unavailable"
else:
state = "unobserved"
return {
"state": state,
**totals,
"avg_latency_ms": round(latency_weight / latency_calls, 1)
if latency_calls
else 0.0,
"models": selected,
}
def provider_status_payload() -> dict[str, Any]:
"""Build the owner-safe status document shared by dashboard and TUI."""
health = _get_json(f"{SWITCHYARD_ROOT}/health")
stats = _get_json(f"{SWITCHYARD_ROOT}/v1/stats")
router_ok = health.get("status") == "ok" and bool(stats)
models = stats.get("models") if isinstance(stats.get("models"), dict) else {}
providers = {
name: _provider_summary(name, models)
for name in ("codex", "claude", "local")
}
providers["codex"]["account"] = _codex_account()
providers["claude"]["account"] = _claude_account()
classifier = stats.get("classifier")
classifier = classifier if isinstance(classifier, dict) else {}
fallbacks = stats.get("routing_fallbacks")
fallbacks = fallbacks if isinstance(fallbacks, dict) else {}
return {
"generated_at": datetime.now(timezone.utc).isoformat(),
"window": "Since the last Switchyard restart",
"quota_note": (
"Codex and Claude subscription balances are not exposed to this "
"router. Open the provider usage page for authoritative remaining "
"capacity; the counters here show actual work observed by Switchyard."
),
"router": {
"state": "available" if router_ok else "unavailable",
"total_requests": _number(stats.get("total_requests")),
"total_errors": _number(stats.get("total_errors")),
"total_tokens": _number(
(stats.get("total_tokens") or {}).get("total")
if isinstance(stats.get("total_tokens"), dict)
else 0
),
"fallbacks": {str(key): _number(value) for key, value in fallbacks.items()},
"classifier_calls": _number(classifier.get("total_requests")),
"classifier_errors": _number(classifier.get("total_errors")),
},
"providers": providers,
}
def provider_status_text() -> str:
"""Render the same status as a compact native Hermes command response."""
payload = provider_status_payload()
router = payload["router"]
labels = {"codex": "Codex", "claude": "Claude", "local": "Local"}
lines = [
f"Provider pool: Switchyard {router['state']}",
str(payload["window"]),
]
for key in ("codex", "claude", "local"):
item = payload["providers"][key]
account = item.get("account") or {}
plan = f" · plan {account.get('plan', 'unknown')}" if account else ""
auth = ""
if account:
if account.get("access_token_live") is False and account.get("refreshable"):
auth = " · auth refreshable"
else:
auth = (
" · auth ready"
if account.get("authenticated")
else " · auth unavailable"
)
lines.append(
f"{labels[key]}: {item['state']} · {item['calls']} completed · "
f"{item['errors']} errors · {item['total_tokens']} tokens{plan}{auth}"
)
lines.extend(
[
f"Fallbacks: {sum(router['fallbacks'].values())} · "
f"classifier: {router['classifier_calls']} calls / "
f"{router['classifier_errors']} errors",
str(payload["quota_note"]),
f"Codex usage: {CODEX_USAGE_URL}",
f"Claude usage: {CLAUDE_USAGE_URL}",
"Dashboard: Provider Status",
]
)
return "\n".join(lines)