mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-03 08:18:43 +02:00
[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:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user