[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
This commit is contained in:
ciregenz
2026-03-26 12:17:37 -07:00
parent bc218c8707
commit e04eb2fb74
7 changed files with 53 additions and 43 deletions
+8 -9
View File
@@ -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:
+5 -6
View File
@@ -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,
+9 -5
View File
@@ -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.")
+27 -1
View File
@@ -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", "")
-19
View File
@@ -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})
+2 -1
View File
@@ -11,4 +11,5 @@ typeguard==4.4.2
python-dotenv==1.1.1
Pillow
posthog
httpx>=0.27.0
httpx>=0.27.0
google-workspace-mcp
+2 -2
View File
@@ -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",