From 693126484bb156677f4cb337b2cc61e93d0ee3c3 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 13 Jun 2026 10:43:57 +0000 Subject: [PATCH] fix(operations): cache the usage endpoint (avoid 429) + show weekly-Sonnet not Opus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups to the usage-% feature: 1. The /api/oauth/usage endpoint 429s when polled often — /operations refreshes every 5s and each refresh hit it (plus the supervisor + ad-hoc calls). Cache the last good payload on the host bridge for 60s and serve it; only re-fetch when stale, so Anthropic sees ~1 req/min regardless of dashboard polling. On a fetch failure (e.g. a transient 429) serve the last good payload instead of blanking the card. The 5-hour window moves slowly, so 60s stays fresh. 2. The third meter showed weekly-Opus, which is null on this account (the per-model weekly cap that's actually populated is Sonnet). Switched the display to seven_day_sonnet / "שבועי · Sonnet". (The supervisor keeps gating on seven_day_opus — the halacha drain runs Opus, so the Opus cap is the correct gate even when it's null/inactive.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../legal_mcp/court_fetch_service/server.py | 31 ++++++++++++++++--- web-ui/src/app/operations/page.tsx | 2 +- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/mcp-server/src/legal_mcp/court_fetch_service/server.py b/mcp-server/src/legal_mcp/court_fetch_service/server.py index 96fb429..28108ff 100644 --- a/mcp-server/src/legal_mcp/court_fetch_service/server.py +++ b/mcp-server/src/legal_mcp/court_fetch_service/server.py @@ -33,6 +33,7 @@ import json import logging import os import sys +import time import aiohttp 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" _OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage" _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: """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: with open(_CLAUDE_CRED_PATH) as f: token = json.load(f)["claudeAiOauth"]["accessToken"] 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) + headers = { "Authorization": f"Bearer {token}", "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 session.get(_OAUTH_USAGE_URL, headers=headers) as r: if r.status != 200: - return web.json_response( - {"error": f"usage endpoint {r.status}"}, status=502) - return web.json_response(await r.json()) - except Exception as e: # never throw + raise RuntimeError(f"usage endpoint {r.status}") + data = await r.json() + except Exception as e: # never throw — serve stale if we have it + 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) + _usage_cache["ts"] = now + _usage_cache["data"] = data + return web.json_response(data) + async def pm2_status(request: web.Request) -> web.Response: """Return a trimmed ``pm2 jlist`` for the legal-ai background services.""" diff --git a/web-ui/src/app/operations/page.tsx b/web-ui/src/app/operations/page.tsx index daa0c22..483e797 100644 --- a/web-ui/src/app/operations/page.tsx +++ b/web-ui/src/app/operations/page.tsx @@ -83,7 +83,7 @@ function SubscriptionUsagePanel({ usage }: { usage: SubscriptionUsage }) {
- +
המנוי המשותף שמפעיל את כל הסוכנים והדריינרים. הפס נצבע בענבר מעל 75% -- 2.49.1