360 lines
14 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"
ROUTING_CATALOG_PATH = Path(
os.environ.get("HERMES_ROUTING_CATALOG_PATH", "/routing-catalog/catalog.json")
)
CLAUDE_HEALTH_PATH = Path(
os.environ.get(
"HERMES_CLAUDE_HEALTH_PATH", "/opt/data/provider-health/claude.json"
)
)
CODEX_HEALTH_PATH = Path(
os.environ.get("HERMES_CODEX_HEALTH_PATH", "/opt/data/provider-health/codex.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")
)
refreshable = bool(tokens.get("refresh_token"))
authenticated = (
bool(tokens.get("access_token")) and token_live is not False
) or refreshable
return {
"authenticated": authenticated,
"access_token_live": token_live,
"refreshable": refreshable,
"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"]
# Switchyard counters span the router process lifetime. One old transient
# failure must not leave an otherwise healthy provider permanently yellow.
total_boundaries = calls + errors
error_ratio = errors / total_boundaries if total_boundaries else 0.0
if calls and error_ratio >= 0.05:
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 _configured_models(provider: str) -> list[str]:
"""Return the stewarded model catalog independently of observed traffic."""
catalog = _read_json(ROUTING_CATALOG_PATH)
providers = catalog.get("providers")
providers = providers if isinstance(providers, dict) else {}
record = providers.get(provider)
record = record if isinstance(record, dict) else {}
models = record.get("models")
if not isinstance(models, list):
return []
return sorted({str(model) for model in models if isinstance(model, str)})
def _fresh_health(path: Path, maximum_age: float = 86400.0) -> dict[str, Any]:
"""Read recent broker health without treating stale state as authoritative."""
value = _read_json(path)
try:
age = time.time() - path.stat().st_mtime
except OSError:
return {}
return value if age <= maximum_age else {}
def _apply_native_health(provider: dict[str, Any], health: dict[str, Any]) -> None:
"""Prefer current native transport health over lifetime router counters."""
provider["native_health"] = health
native_state = health.get("state")
if native_state == "available":
provider["state"] = "available"
elif native_state in {"capacity-limited", "degraded"}:
provider["state"] = "degraded"
elif native_state == "unavailable":
provider["state"] = "unavailable"
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")
}
for name, item in providers.items():
item["configured_models"] = _configured_models(name)
item["supported_efforts"] = (
["low", "medium", "high", "xhigh"]
if name in {"codex", "claude"}
else ["medium"]
)
providers["codex"]["account"] = _codex_account()
providers["claude"]["account"] = _claude_account()
_apply_native_health(providers["codex"], _fresh_health(CODEX_HEALTH_PATH))
_apply_native_health(providers["claude"], _fresh_health(CLAUDE_HEALTH_PATH))
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 uses the owner's ChatGPT Codex OAuth and Claude uses the "
"owner's native first-party Claude Code subscription; the Claude "
"lane does not use the metered Anthropic API key. Open each official "
"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)