From e840b1b7f0dc0f4773ccb86fc977bd419181da55 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 13 May 2026 19:05:04 -0700 Subject: [PATCH] [eric] sub bug fix --- backend/apps/agents/agents.py | 68 ++++++++++++++++++++--- backend/apps/agents/providers/registry.py | 11 +++- electron/package-lock.json | 4 +- electron/package.json | 2 +- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index 85cfc032..bc0c0ea7 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -295,6 +295,18 @@ async def subscriptions_connect(body: dict): if not is_running(): raise HTTPException(status_code=503, detail="9Router not available. Please install Node.js.") + # If reconnecting a primary lane (e.g. gemini-cli), drop its cascade + # siblings first. The registry prefers antigravity over gemini-cli + # when both are present, so a stale antigravity token would keep + # 400ing even after gemini-cli refreshes. Wiping the sibling forces + # the registry onto the freshly reconnected lane. + cascade = _PROVIDER_CASCADE_REMOVES.get(provider, []) + if cascade: + try: + await _delete_provider_connections(cascade) + except Exception: + pass + try: result = await start_oauth(provider) @@ -680,21 +692,61 @@ async def list_models(): return {"models": result, "notes": notes} +# Google's two OAuth lanes (gemini-cli and antigravity) share user-facing +# meaning (both = "Google subscription") but 9Router treats them as +# separate connections with independent token lifecycles. The registry +# prefers `ag/` over `gc/` whenever AG is active because AG bypasses the +# thoughtSignature validator that breaks multi-step tool turns. That +# preference becomes a footgun when AG's token expires silently: the +# user reconnects "Google", only gemini-cli refreshes, and every request +# still routes through the stale AG token -> 400 Invalid argument. +# +# Cascade is one-directional. gemini-cli is the primary lane the UI +# exposes; operations on it sweep antigravity too. Direct operations on +# antigravity (e.g. an explicit AG opt-in/out path) MUST NOT cascade +# back to gemini-cli or we'd nuke the user's main Google connection. +_PROVIDER_CASCADE_REMOVES: dict[str, list[str]] = { + "gemini-cli": ["antigravity"], +} + + +async def _delete_provider_connections(providers: list[str]) -> int: + """Delete all 9Router connections whose provider is in the given list. + Returns the count actually removed. Silent if 9Router is unreachable.""" + import httpx + from backend.apps.nine_router import NINE_ROUTER_API, get_providers + try: + connections = await get_providers() + except Exception: + return 0 + targets = [c for c in connections if c.get("provider") in providers and c.get("id")] + removed = 0 + async with httpx.AsyncClient(timeout=10.0) as client: + for c in targets: + try: + await client.delete(f"{NINE_ROUTER_API}/providers/{c['id']}") + removed += 1 + except Exception: + pass + return removed + + @agents.router.post("/subscriptions/disconnect") async def subscriptions_disconnect(body: dict): - """Disconnect a subscription provider via 9Router.""" - import httpx + """Disconnect a subscription provider via 9Router. + + For Google's paired lanes (gemini-cli + antigravity), wipe BOTH so a + subsequent reconnect lands on a clean slate instead of resurrecting + a stale sibling. + """ provider = body.get("provider", "") if not provider: raise HTTPException(status_code=400, detail="provider required") try: - from backend.apps.nine_router import NINE_ROUTER_API, get_providers - connections = await get_providers() - conn = next((c for c in connections if c.get("provider") == provider), None) - if conn and conn.get("id"): - async with httpx.AsyncClient(timeout=10.0) as client: - await client.delete(f"{NINE_ROUTER_API}/providers/{conn['id']}") + to_remove = [provider, *_PROVIDER_CASCADE_REMOVES.get(provider, [])] + removed = await _delete_provider_connections(to_remove) + if removed: from backend.apps.service.client import sync as _sync from backend.apps.settings.settings import load_settings _sync(load_settings().model_dump()) diff --git a/backend/apps/agents/providers/registry.py b/backend/apps/agents/providers/registry.py index 6355a306..0136e3f9 100644 --- a/backend/apps/agents/providers/registry.py +++ b/backend/apps/agents/providers/registry.py @@ -416,14 +416,19 @@ def resolve_model_id_for_sdk(short_name: str, settings: AppSettings) -> str: return entry.get("model_id", short_name) if getattr(settings, "anthropic_api_key", None): return entry.get("model_id", short_name) - # Gemini lane order: AI Studio apikey → Antigravity OAuth → Gemini CLI. + # Gemini lane order: AI Studio apikey, Antigravity OAuth, Gemini CLI. # AG bypasses the thoughtSignature validator that breaks multi-step tool # turns on gc/. Without it, every Gemini turn 400s after the first tool # call with "Thought signature is not valid". _ANTIGRAVITY_MAP = { - # gemini-3-pro-preview disabled — AG returns 404 even with active conn. + # gemini-3-pro-preview disabled: AG returns 404 even with active conn. + # gemini-3.1-pro-preview disabled: AG's `gemini-3.1-pro-high` variant + # 400s every request with "invalid argument" (the `-high` thinking- + # budget alias on AG requires a thinking_config the CLI doesn't + # emit). Falls through to gc/gemini-3.1-pro-preview, which works + # for non-tool turns; multi-step tool turns still hit the + # thoughtSignature validator but that's a separate fight. "gemini-3-flash-preview": "gemini-3-flash", - "gemini-3.1-pro-preview": "gemini-3.1-pro-high", "gemini-3.1-flash-lite-preview": "gemini-3-flash", } if entry.get("api") == "gemini-cli": diff --git a/electron/package-lock.json b/electron/package-lock.json index db21a5bd..46091a05 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "openswarm", - "version": "1.0.32", + "version": "1.1.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openswarm", - "version": "1.0.32", + "version": "1.1.32", "hasInstallScript": true, "dependencies": { "electron-updater": "^6.3.0", diff --git a/electron/package.json b/electron/package.json index 02f3f203..8529fbfc 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "openswarm", - "version": "1.0.32", + "version": "1.1.32", "description": "OpenSwarm — AI Agent Orchestrator", "main": "main.js", "scripts": {