From 97622f87e091c90799178fa84f89ec94b0049159 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 27 Mar 2026 13:06:17 -0700 Subject: [PATCH] [eric] fix API key priority over 9Router, fix OAuth callback 500, polish Settings UI --- 9router/next.config.mjs | 2 + backend/apps/agents/agent_manager.py | 8 +- backend/apps/agents/providers/registry.py | 1 + backend/apps/analytics/analytics.py | 2 +- backend/apps/nine_router.py | 29 +++- backend/apps/settings/credentials.py | 21 +-- electron/package-lock.json | 4 +- electron/package.json | 2 +- frontend/src/app/pages/Settings/Settings.tsx | 141 ++++++++++++++++--- 9 files changed, 171 insertions(+), 39 deletions(-) diff --git a/9router/next.config.mjs b/9router/next.config.mjs index 36505ea0..04a5a26a 100644 --- a/9router/next.config.mjs +++ b/9router/next.config.mjs @@ -5,6 +5,8 @@ const nextConfig = { unoptimized: true }, env: {}, + // Allow builds with both Turbopack (Next 16 default) and webpack + turbopack: {}, webpack: (config, { isServer }) => { // Ignore fs/path modules in browser bundle if (!isServer) { diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 398ffbd8..b2992daf 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -862,17 +862,17 @@ class AgentManager: "disallowed_tools": effective_disallowed, "include_partial_messages": True, } - # Priority: 9Router subscription → API key + # Priority: API key → 9Router subscription from backend.apps.nine_router import is_running as _9r_running - if _9r_running(): + if global_settings.anthropic_api_key: + options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key} + elif _9r_running(): options_kwargs["env"] = { "ANTHROPIC_API_KEY": "9router", "ANTHROPIC_BASE_URL": "http://localhost:20128", } # --bare skips CLI's own OAuth/keychain auth, uses only ANTHROPIC_API_KEY options_kwargs["extra_args"] = {"bare": None} - elif global_settings.anthropic_api_key: - options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key} else: raise ValueError("No AI provider configured. Set an API key or connect a subscription.") if mcp_servers: diff --git a/backend/apps/agents/providers/registry.py b/backend/apps/agents/providers/registry.py index 947794b9..7ebe4c8e 100644 --- a/backend/apps/agents/providers/registry.py +++ b/backend/apps/agents/providers/registry.py @@ -114,6 +114,7 @@ def create_provider( auth_token=getattr(settings, "openswarm_auth_token", None), base_url=getattr(settings, "openswarm_proxy_url", None) or "https://api.openswarm.ai", ) + # Priority: API key → 9Router subscription if settings.anthropic_api_key: return AnthropicProvider(api_key=settings.anthropic_api_key) # No API key — try 9Router as fallback diff --git a/backend/apps/analytics/analytics.py b/backend/apps/analytics/analytics.py index d4f56023..1b172af9 100644 --- a/backend/apps/analytics/analytics.py +++ b/backend/apps/analytics/analytics.py @@ -15,7 +15,7 @@ from backend.apps.analytics.collector import init as init_collector, shutdown as logger = logging.getLogger(__name__) -APP_VERSION = "1.0.16" +APP_VERSION = "1.0.17" _heartbeat_task: asyncio.Task | None = None diff --git a/backend/apps/nine_router.py b/backend/apps/nine_router.py index 65158703..727ba253 100644 --- a/backend/apps/nine_router.py +++ b/backend/apps/nine_router.py @@ -76,11 +76,32 @@ def _find_node() -> str | None: async def ensure_running(): """Start 9Router if not already running.""" global _process - if is_running(): - logger.info("9Router already running on port %d", NINE_ROUTER_PORT) - return - _is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" + + if is_running(): + # In dev mode, kill stale standalone servers (from previous builds) + # so we can start `next dev` which always uses latest source code + if not _is_packaged: + import subprocess as _sp + try: + result = _sp.run( + ["pgrep", "-f", "next-server"], + capture_output=True, text=True, timeout=3, + ) + if result.stdout.strip(): + logger.info("Dev mode: killing stale standalone 9Router to use next dev instead") + _sp.run(["pkill", "-f", "next-server"], timeout=5) + import asyncio + await asyncio.sleep(2) + else: + logger.info("9Router already running on port %d", NINE_ROUTER_PORT) + return + except Exception: + logger.info("9Router already running on port %d", NINE_ROUTER_PORT) + return + else: + logger.info("9Router already running on port %d", NINE_ROUTER_PORT) + return _9router_dir = _find_9router_dir() if _is_packaged and _9router_dir: diff --git a/backend/apps/settings/credentials.py b/backend/apps/settings/credentials.py index 59f6cf25..60026faf 100644 --- a/backend/apps/settings/credentials.py +++ b/backend/apps/settings/credentials.py @@ -77,9 +77,10 @@ def validate_credentials(settings: AppSettings, provider: str = "anthropic") -> def get_provider_credentials(settings: AppSettings, provider: str) -> dict[str, str]: """Return credential dict for a specific provider.""" + p = provider.lower().strip() validate_credentials(settings, provider) - if provider == "anthropic": + if p in ("anthropic", "claude"): if getattr(settings, "connection_mode", "own_key") == "managed": return { "auth_token": getattr(settings, "openswarm_auth_token", "") or "", @@ -87,18 +88,18 @@ def get_provider_credentials(settings: AppSettings, provider: str) -> dict[str, } return {"api_key": settings.anthropic_api_key or ""} - if provider == "openai": + if p in ("openai", "codex"): return {"api_key": settings.openai_api_key or ""} - if provider == "gemini": + if p in ("gemini", "google", "gemini-cli"): return {"api_key": getattr(settings, "google_api_key", "") or ""} - if provider == "openrouter": + if p == "openrouter": return {"api_key": getattr(settings, "openrouter_api_key", "") or ""} # Custom provider for cp in getattr(settings, "custom_providers", []): - if cp.name == provider: + if cp.name.lower() == p: return {"api_key": cp.api_key, "base_url": cp.base_url} raise ValueError(f"No credentials for provider: {provider}") @@ -139,15 +140,15 @@ def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic: base_url=proxy_url, ) - # Prefer 9Router subscription (free for users with Claude/ChatGPT/Gemini subscriptions) + # Prefer API key when set + if settings.anthropic_api_key: + return anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) + + # Fall back to 9Router subscription (free for users with Claude/ChatGPT/Gemini subscriptions) if _check_9router(): return anthropic.AsyncAnthropic( api_key="9router", base_url="http://localhost:20128", ) - # Fall back to API key - if settings.anthropic_api_key: - return anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) - raise ValueError("No AI provider configured. Set an API key or connect a subscription.") diff --git a/electron/package-lock.json b/electron/package-lock.json index 787a48eb..09d06c31 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "openswarm", - "version": "1.0.15", + "version": "1.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openswarm", - "version": "1.0.15", + "version": "1.0.16", "hasInstallScript": true, "dependencies": { "electron-updater": "^6.3.0", diff --git a/electron/package.json b/electron/package.json index 38b59602..bfec9d2d 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "openswarm", - "version": "1.0.16", + "version": "1.0.17", "description": "OpenSwarm — AI Agent Orchestrator", "main": "main.js", "scripts": { diff --git a/frontend/src/app/pages/Settings/Settings.tsx b/frontend/src/app/pages/Settings/Settings.tsx index 20f23108..f4bfb0f4 100644 --- a/frontend/src/app/pages/Settings/Settings.tsx +++ b/frontend/src/app/pages/Settings/Settings.tsx @@ -164,17 +164,36 @@ const SUBSCRIPTION_PROVIDERS = [ { id: 'github', name: 'GitHub Copilot', desc: 'Claude + GPT models via your Copilot subscription', color: '#8B949E', preview: true }, ]; -const SubscriptionCard: React.FC<{ provider: typeof SUBSCRIPTION_PROVIDERS[0]; connected: boolean; onConnect: () => void; onDisconnect: () => void; connecting: boolean; userCode?: string }> = ({ provider, connected, onConnect, onDisconnect, connecting, userCode }) => { +const SubscriptionCard: React.FC<{ provider: typeof SUBSCRIPTION_PROVIDERS[0]; connected: boolean; onConnect: () => void; onDisconnect: () => void; connecting: boolean; userCode?: string; disconnecting?: boolean }> = ({ provider, connected, onConnect, onDisconnect, connecting, userCode, disconnecting }) => { const c = useClaudeTokens(); const isPreview = (provider as any).preview; return ( - + - + {provider.name} - {provider.desc} + + {connecting ? 'Waiting for authorization...' : provider.desc} + {isPreview ? ( @@ -182,17 +201,26 @@ const SubscriptionCard: React.FC<{ provider: typeof SUBSCRIPTION_PROVIDERS[0]; c Coming soon ) : connected ? ( - - Disconnect - + disconnecting ? ( + + ) : ( + + Disconnect + + ) ) : connecting && userCode ? ( Enter code: {userCode} + ) : connecting ? ( + + + Connecting... + ) : ( - )} @@ -204,6 +232,7 @@ const SubscriptionCards: React.FC = () => { const c = useClaudeTokens(); const [status, setStatus] = useState(null); const [connecting, setConnecting] = useState(null); + const [disconnecting, setDisconnecting] = useState(null); const [userCode, setUserCode] = useState(''); const [pollTimer, setPollTimer] = useState(null); @@ -323,6 +352,7 @@ const SubscriptionCards: React.FC = () => { }; const handleDisconnect = async (providerId: string) => { + setDisconnecting(providerId); try { await fetch(`${API_BASE}/agents/subscriptions/disconnect`, { method: 'POST', @@ -331,13 +361,36 @@ const SubscriptionCards: React.FC = () => { }); } catch {} // Wait briefly for 9Router to process, then refresh - setTimeout(fetchStatus, 500); + setTimeout(() => { fetchStatus(); setDisconnecting(null); }, 500); }; + if (!status) { + // Initial loading — show skeleton cards + return ( + + {SUBSCRIPTION_PROVIDERS.map(p => ( + + + + + + + + ))} + + ); + } + if (!status?.running) { return ( - + + Starting subscription service... @@ -357,6 +410,7 @@ const SubscriptionCards: React.FC = () => { onConnect={() => handleConnect(p.id)} onDisconnect={() => handleDisconnect(p.id)} connecting={connecting === p.id} + disconnecting={disconnecting === p.id} userCode={connecting === p.id ? userCode : undefined} /> ))} @@ -401,7 +455,59 @@ const UsageStats: React.FC = () => { .catch(() => {}); }, []); - if (!stats) return null; + if (!stats) { + // Skeleton loading state + const skeletonPulse = { + animation: 'skeleton-pulse 1.5s ease-in-out infinite', + '@keyframes skeleton-pulse': { '0%, 100%': { opacity: 0.5 }, '50%': { opacity: 0.25 } }, + }; + const skeletonCard = { + p: 1.5, borderRadius: `${c.radius.md}px`, bgcolor: c.bg.elevated, + border: `1px solid ${c.border.subtle}`, ...skeletonPulse, + }; + return ( + + + {Array.from({ length: 4 }, (_, i) => ( + + + + + + ))} + + + {Array.from({ length: 4 }, (_, i) => ( + + + + + + ))} + + + {Array.from({ length: 2 }, (_, i) => ( + + + {Array.from({ length: 3 }, (_, j) => ( + + + + + + + {Array.from({ length: 16 }, (_, k) => ( + + ))} + + + ))} + + ))} + + + ); + } const formatCost = (v: number) => { if (v === 0) return '$0.00'; @@ -757,11 +863,12 @@ const Settings: React.FC = () => { PaperProps={{ sx: { width: 780, - maxHeight: '85vh', + height: '85vh', bgcolor: c.bg.page, borderRadius: 2, border: `1px solid ${c.border.subtle}`, boxShadow: c.shadow.md, + transition: 'none', }, }} > @@ -814,7 +921,7 @@ const Settings: React.FC = () => { scrollbarColor: `${c.border.medium} transparent`, }}> {activeTab === 'general' ? ( - + {/* ── Agent Defaults ── */} Agent Defaults @@ -1283,7 +1390,7 @@ const Settings: React.FC = () => { ) : activeTab === 'models' ? ( - + {/* ── USE EXISTING SUBSCRIPTIONS ── */} @@ -1347,11 +1454,11 @@ const Settings: React.FC = () => { ) : activeTab === 'usage' ? ( - + ) : ( - + )}