fix(operations): cache usage endpoint (avoid 429) + weekly-Sonnet instead of Opus #245
@@ -33,6 +33,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@@ -102,16 +103,31 @@ async def _pm2_run(*args: str, timeout: float = 10) -> tuple[int, bytes, bytes]:
|
|||||||
_CLAUDE_CRED_PATH = "/home/chaim/.claude/.credentials.json"
|
_CLAUDE_CRED_PATH = "/home/chaim/.claude/.credentials.json"
|
||||||
_OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
|
_OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
|
||||||
_USAGE_UA = "claude-code/2.1.177"
|
_USAGE_UA = "claude-code/2.1.177"
|
||||||
|
# /operations polls every 5s; the usage endpoint 429s if hit that often (it's
|
||||||
|
# meant for a status bar, not a poll loop). Cache the last good payload and only
|
||||||
|
# re-fetch when older than this — Anthropic sees ~1 req/min regardless of how
|
||||||
|
# many dashboards poll. The 5-hour window moves slowly, so 60s is plenty fresh.
|
||||||
|
_USAGE_TTL_SEC = 60.0
|
||||||
|
_usage_cache: dict = {"ts": 0.0, "data": None}
|
||||||
|
|
||||||
|
|
||||||
async def usage_status(request: web.Request) -> web.Response:
|
async def usage_status(request: web.Request) -> web.Response:
|
||||||
"""Proxy the claude.ai subscription usage % (host-only — needs the local
|
"""Proxy the claude.ai subscription usage % (host-only — needs the local
|
||||||
OAuth token). Returns the endpoint's JSON, or a 502 with an error string."""
|
OAuth token), cached for _USAGE_TTL_SEC. On a fetch failure (e.g. the
|
||||||
|
endpoint's own 429) serve the last good payload if we have one, so a
|
||||||
|
transient limit doesn't blank the dashboard."""
|
||||||
|
now = time.monotonic()
|
||||||
|
if _usage_cache["data"] is not None and (now - _usage_cache["ts"]) < _USAGE_TTL_SEC:
|
||||||
|
return web.json_response(_usage_cache["data"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(_CLAUDE_CRED_PATH) as f:
|
with open(_CLAUDE_CRED_PATH) as f:
|
||||||
token = json.load(f)["claudeAiOauth"]["accessToken"]
|
token = json.load(f)["claudeAiOauth"]["accessToken"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if _usage_cache["data"] is not None:
|
||||||
|
return web.json_response(_usage_cache["data"])
|
||||||
return web.json_response({"error": f"no claude credentials: {e}"}, status=502)
|
return web.json_response({"error": f"no claude credentials: {e}"}, status=502)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
"User-Agent": _USAGE_UA,
|
"User-Agent": _USAGE_UA,
|
||||||
@@ -122,12 +138,17 @@ async def usage_status(request: web.Request) -> web.Response:
|
|||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
async with session.get(_OAUTH_USAGE_URL, headers=headers) as r:
|
async with session.get(_OAUTH_USAGE_URL, headers=headers) as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
return web.json_response(
|
raise RuntimeError(f"usage endpoint {r.status}")
|
||||||
{"error": f"usage endpoint {r.status}"}, status=502)
|
data = await r.json()
|
||||||
return web.json_response(await r.json())
|
except Exception as e: # never throw — serve stale if we have it
|
||||||
except Exception as e: # never throw
|
if _usage_cache["data"] is not None:
|
||||||
|
return web.json_response(_usage_cache["data"])
|
||||||
return web.json_response({"error": f"usage fetch failed: {e}"}, status=502)
|
return web.json_response({"error": f"usage fetch failed: {e}"}, status=502)
|
||||||
|
|
||||||
|
_usage_cache["ts"] = now
|
||||||
|
_usage_cache["data"] = data
|
||||||
|
return web.json_response(data)
|
||||||
|
|
||||||
|
|
||||||
async def pm2_status(request: web.Request) -> web.Response:
|
async def pm2_status(request: web.Request) -> web.Response:
|
||||||
"""Return a trimmed ``pm2 jlist`` for the legal-ai background services."""
|
"""Return a trimmed ``pm2 jlist`` for the legal-ai background services."""
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function SubscriptionUsagePanel({ usage }: { usage: SubscriptionUsage }) {
|
|||||||
<div className="grid gap-6 sm:grid-cols-3">
|
<div className="grid gap-6 sm:grid-cols-3">
|
||||||
<UsageMeter label="חלון 5-שעות" w={usage.five_hour} />
|
<UsageMeter label="חלון 5-שעות" w={usage.five_hour} />
|
||||||
<UsageMeter label="שבועי · כל המודלים" w={usage.seven_day} />
|
<UsageMeter label="שבועי · כל המודלים" w={usage.seven_day} />
|
||||||
<UsageMeter label="שבועי · Opus" w={usage.seven_day_opus} />
|
<UsageMeter label="שבועי · Sonnet" w={usage.seven_day_sonnet} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 border-t border-rule-soft pt-2.5 text-[11.5px] text-ink-muted">
|
<div className="mt-3 border-t border-rule-soft pt-2.5 text-[11.5px] text-ink-muted">
|
||||||
המנוי המשותף שמפעיל את כל הסוכנים והדריינרים. הפס נצבע בענבר מעל 75%
|
המנוי המשותף שמפעיל את כל הסוכנים והדריינרים. הפס נצבע בענבר מעל 75%
|
||||||
|
|||||||
Reference in New Issue
Block a user