From e04eb2fb743fc9ee85cc59e22e2ae432626a0cc3 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 26 Mar 2026 12:17:37 -0700 Subject: [PATCH] [eric] fix browser agent for 9Router, subscription-first priority, uvx fallback for non-dev users - browser_agent.py: use get_anthropic_client instead of raw api_key, fix model map for 9Router - credentials.py + agent_manager.py: subscription (9Router) checked before API key everywhere - main.py: clean browser-agent endpoint, no credential params - tools_lib.py: when uvx not found, fall back to pip-installed binary from venv - requirements.txt: add google-workspace-mcp so it works without uvx --- backend/apps/agents/agent_manager.py | 17 ++++++++--------- backend/apps/agents/browser_agent.py | 11 +++++------ backend/apps/settings/credentials.py | 14 +++++++++----- backend/apps/tools_lib/tools_lib.py | 28 +++++++++++++++++++++++++++- backend/main.py | 19 ------------------- backend/requirements.txt | 3 ++- electron/package-lock.json | 4 ++-- 7 files changed, 53 insertions(+), 43 deletions(-) diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index c709f8ca..46f7f89e 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -835,17 +835,16 @@ class AgentManager: "disallowed_tools": effective_disallowed, "include_partial_messages": True, } - if global_settings.anthropic_api_key: + # Priority: 9Router subscription → API key + from backend.apps.nine_router import is_running as _9r_running + if _9r_running(): + options_kwargs["env"] = { + "ANTHROPIC_BASE_URL": "http://localhost:20128", + } + elif global_settings.anthropic_api_key: options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key} else: - # Try 9Router as fallback for subscription access - from backend.apps.nine_router import is_running as _9r_running - if _9r_running(): - options_kwargs["env"] = { - "ANTHROPIC_BASE_URL": "http://localhost:20128", - } - else: - raise ValueError("No AI provider configured. Set an API key or connect a subscription.") + raise ValueError("No AI provider configured. Set an API key or connect a subscription.") if mcp_servers: options_kwargs["mcp_servers"] = mcp_servers if composed_prompt: diff --git a/backend/apps/agents/browser_agent.py b/backend/apps/agents/browser_agent.py index 9173e75a..7ac3d2a7 100644 --- a/backend/apps/agents/browser_agent.py +++ b/backend/apps/agents/browser_agent.py @@ -22,8 +22,8 @@ from backend.apps.tools_lib.tools_lib import load_builtin_permissions logger = logging.getLogger(__name__) MODEL_MAP = { - "sonnet": "claude-sonnet-4-20250514", - "opus": "claude-opus-4-20250514", + "sonnet": "claude-sonnet-4-6", + "opus": "claude-opus-4-6", "haiku": "claude-haiku-4-5-20251001", } @@ -278,7 +278,6 @@ async def run_browser_agent( task: str, browser_id: str, model: str, - api_key: str, dashboard_id: str | None = None, tab_id: str = "", pre_selected: bool = False, @@ -323,7 +322,9 @@ async def run_browser_agent( logger.info(f"Browser agent {session_id}: navigated to {initial_url}: {nav_result.get('text', nav_result.get('error', ''))}") api_model = MODEL_MAP.get(model, model) - client = anthropic.AsyncAnthropic(api_key=api_key) + from backend.apps.settings.settings import load_settings + from backend.apps.settings.credentials import get_anthropic_client + client = get_anthropic_client(load_settings()) messages: list[dict] = [{"role": "user", "content": task}] action_log: list[dict] = [] @@ -582,7 +583,6 @@ async def _create_browser_card(dashboard_id: str, url: str, parent_session_id: s async def run_browser_agents( tasks: list[dict], model: str, - api_key: str, dashboard_id: str | None = None, pre_selected_browser_ids: list[str] | None = None, parent_session_id: str | None = None, @@ -608,7 +608,6 @@ async def run_browser_agents( task=task_text, browser_id=browser_id, model=model, - api_key=api_key, dashboard_id=dashboard_id, pre_selected=is_pre_selected, initial_url=url if url and browser_id not in pre_selected else None, diff --git a/backend/apps/settings/credentials.py b/backend/apps/settings/credentials.py index 58c18f04..59f6cf25 100644 --- a/backend/apps/settings/credentials.py +++ b/backend/apps/settings/credentials.py @@ -126,7 +126,10 @@ def get_agent_sdk_env(settings: AppSettings) -> dict[str, str]: def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic: - """Return a configured AsyncAnthropic client based on connection mode.""" + """Return a configured AsyncAnthropic client based on connection mode. + + Priority: managed mode → 9Router subscription → API key + """ import anthropic if getattr(settings, "connection_mode", "own_key") == "managed": @@ -136,14 +139,15 @@ def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic: base_url=proxy_url, ) - if settings.anthropic_api_key: - return anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) - - # Fallback to 9Router + # Prefer 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/backend/apps/tools_lib/tools_lib.py b/backend/apps/tools_lib/tools_lib.py index e245cac7..d2fe30d1 100644 --- a/backend/apps/tools_lib/tools_lib.py +++ b/backend/apps/tools_lib/tools_lib.py @@ -323,7 +323,8 @@ def _extra_bin_dirs() -> list[str]: def _resolve_command(command: str) -> str | None: - """Find a command on PATH, falling back to common user-local bin directories.""" + """Find a command on PATH, falling back to common user-local bin directories + and the bundled Python environment.""" found = shutil.which(command) if found: return found @@ -331,6 +332,20 @@ def _resolve_command(command: str) -> str | None: candidate = os.path.join(d, command) if os.path.isfile(candidate) and os.access(candidate, os.X_OK): return candidate + # Check the bundled Python venv (for pip-installed MCP servers like google-workspace-worker) + _is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" + if _is_packaged: + # In packaged app, check python-env/bin/ + _resources = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + candidate = os.path.join(_resources, "python-env", "bin", command) + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + else: + # In dev, check the backend venv (tools_lib.py is at backend/apps/tools_lib/) + _backend = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + candidate = os.path.join(_backend, ".venv", "bin", command) + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate return None @@ -388,6 +403,17 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]: resolved = _resolve_command(config["command"]) if resolved: config["command"] = resolved + elif config["command"] in ("uvx", "pipx"): + # uvx/pipx not installed — try to find the target binary directly + # uvx args: ["--from", "package-name", "binary-name"] + args = config.get("args", []) + if len(args) >= 3 and args[0] == "--from": + binary_name = args[2] + resolved_binary = _resolve_command(binary_name) + if resolved_binary: + config["command"] = resolved_binary + config["args"] = args[3:] # strip --from pkg binary + logger.info(f"uvx fallback: using installed {binary_name} at {resolved_binary}") env = config.setdefault("env", {}) env.setdefault("PATH", _augmented_path()) env.setdefault("PYTHONPATH", "") diff --git a/backend/main.py b/backend/main.py index f3af2e35..2d1284c3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -186,31 +186,12 @@ async def browser_agent_run(request: Request): if not tasks: return JSONResponse({"error": "tasks array is required"}, status_code=400) - settings = load_settings() - - # Determine API credentials — check API key, then 9Router - api_key = settings.anthropic_api_key - auth_token = None - base_url = None - - if not api_key: - # Try 9Router - from backend.apps.nine_router import is_running as _9r_running - if _9r_running(): - api_key = "9router" - base_url = "http://localhost:20128/v1" - else: - return JSONResponse({"error": "No AI provider configured. Set an API key or connect a subscription."}, status_code=400) - results = await run_browser_agents( tasks=tasks, model=model, - api_key=api_key, dashboard_id=dashboard_id or None, pre_selected_browser_ids=pre_selected_browser_ids, parent_session_id=parent_session_id or None, - auth_token=auth_token, - base_url=base_url, ) return JSONResponse({"results": results}) diff --git a/backend/requirements.txt b/backend/requirements.txt index 909d7685..bccdbeb7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,4 +11,5 @@ typeguard==4.4.2 python-dotenv==1.1.1 Pillow posthog -httpx>=0.27.0 \ No newline at end of file +httpx>=0.27.0 +google-workspace-mcp \ No newline at end of file diff --git a/electron/package-lock.json b/electron/package-lock.json index 76236be9..f6ed211e 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "openswarm", - "version": "1.0.13", + "version": "1.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openswarm", - "version": "1.0.13", + "version": "1.0.14", "hasInstallScript": true, "dependencies": { "electron-updater": "^6.3.0",