mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] health: one banner line per provider, a 401 naming its own reset window is mid-refresh not dead
This commit is contained in:
@@ -57,6 +57,10 @@ def classify_auth_dead(status_code: int, body_text: str) -> bool:
|
||||
if status_code not in (401, 403):
|
||||
return False
|
||||
low = body_text.lower()
|
||||
# A 401 that names its own recovery window ("reset after 1m 57s") is a token mid-refresh, not a
|
||||
# dead login; it heals itself and the banner would cry wolf while real chats work (caught live).
|
||||
if "reset after" in low or "try again in" in low:
|
||||
return False
|
||||
return any(m in low for m in P_AUTH_DEAD_MARKERS)
|
||||
|
||||
|
||||
@@ -101,10 +105,17 @@ async def probe_subscription_health(connections: List[Dict]) -> List[Dict[str, s
|
||||
async with p_probe_lock:
|
||||
if p_cached_result is not None and time.monotonic() - p_cached_at < P_CACHE_TTL_S:
|
||||
return p_cached_result
|
||||
subs = [
|
||||
c for c in connections
|
||||
if isinstance(c, dict) and c.get("provider") in PREFIX_BY_PROVIDER and c.get("isActive")
|
||||
]
|
||||
# One probe per PROVIDER: db.json can hold several active rows for one provider (a stale +
|
||||
# a fresh connect), and probing per row reported "ChatGPT and ChatGPT" in the banner.
|
||||
p_seen: set = set()
|
||||
subs = []
|
||||
for c in connections:
|
||||
if not (isinstance(c, dict) and c.get("provider") in PREFIX_BY_PROVIDER and c.get("isActive")):
|
||||
continue
|
||||
if c.get("provider") in p_seen:
|
||||
continue
|
||||
p_seen.add(c.get("provider"))
|
||||
subs.append(c)
|
||||
dead: List[Dict[str, str]] = []
|
||||
if subs:
|
||||
async with httpx.AsyncClient(timeout=P_PROBE_TIMEOUT_S) as client:
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""One banner line per provider: two active db.json rows for one provider (stale + fresh connect)
|
||||
used to probe twice and render "Your ChatGPT and ChatGPT logins have expired"."""
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.apps.nine_router import subscription_health as sh
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_provider_rows_probe_and_report_once(monkeypatch):
|
||||
sh.invalidate_health_cache()
|
||||
monkeypatch.setattr(sh, "is_running", lambda: True)
|
||||
probed = []
|
||||
|
||||
async def fake_pick(client, prefix):
|
||||
return prefix + "model"
|
||||
|
||||
async def fake_probe(client, model):
|
||||
probed.append(model)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(sh, "p_pick_probe_model", fake_pick)
|
||||
monkeypatch.setattr(sh, "p_probe_one", fake_probe)
|
||||
conns = [
|
||||
{"provider": "codex", "isActive": True, "id": "stale"},
|
||||
{"provider": "codex", "isActive": True, "id": "fresh"},
|
||||
{"provider": "claude", "isActive": True, "id": "c1"},
|
||||
]
|
||||
dead = await sh.probe_subscription_health(conns)
|
||||
assert probed == ["cx/model", "cc/model"], "one probe per provider, not per row"
|
||||
assert [d["label"] for d in dead] == ["ChatGPT", "Claude"], "labels never repeat"
|
||||
sh.invalidate_health_cache()
|
||||
|
||||
|
||||
def test_self_healing_401_with_reset_window_is_not_dead():
|
||||
# Verbatim live body (2026-08-06): the codex lane 401s while its token is mid-refresh, then heals.
|
||||
body = '{"error":{"message":"[codex/gpt-5.2] [401]: Provided authentication token is expired. Please try signing in again. (reset after 1m 57s)"}}'
|
||||
assert not sh.classify_auth_dead(401, body)
|
||||
|
||||
|
||||
def test_genuine_rotation_death_still_reports():
|
||||
assert sh.classify_auth_dead(401, "invalid_grant: refresh token rotated")
|
||||
assert sh.classify_auth_dead(403, "Unauthorized: expired credentials, please sign in")
|
||||
@@ -23,7 +23,8 @@ export default function ProviderHealthToast() {
|
||||
dispatch(hideProviderHealthToast());
|
||||
}, [dispatch]);
|
||||
|
||||
const labels = dead.map((d) => d.label).join(' and ');
|
||||
// Defensive dedupe: duplicate provider rows upstream once rendered "ChatGPT and ChatGPT".
|
||||
const labels = Array.from(new Set(dead.map((d) => d.label))).join(' and ');
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
|
||||
Reference in New Issue
Block a user