""" NeuroSploit v3 - Providers API REST endpoints for managing LLM providers and accounts. """ from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import Optional router = APIRouter() class ConnectRequest(BaseModel): label: str = "Manual API Key" credential: str credential_type: str = "api_key" model_override: Optional[str] = None @router.get("") async def list_providers(): """List all providers with their accounts and status.""" from backend.core.smart_router import get_registry registry = get_registry() if not registry: return {"enabled": False, "providers": []} providers = [] for p in registry.get_all_providers(): accounts = [] for a in p.accounts.values(): accounts.append({ "id": a.id, "label": a.label, "source": a.source, "credential_type": a.credential_type, "is_active": a.is_active, "tokens_used": a.tokens_used, "last_used": a.last_used, "expires_at": a.expires_at, "model_override": a.model_override, }) providers.append({ "id": p.id, "name": p.name, "auth_type": p.auth_type, "api_format": p.api_format, "tier": p.tier, "default_model": p.default_model, "accounts": accounts, "connected": any( a.is_active and a.id in registry._credentials for a in p.accounts.values() ), "enabled": getattr(p, "enabled", True), }) return {"enabled": True, "providers": providers} @router.get("/status") async def providers_status(): """Get quota and usage summary.""" from backend.core.smart_router import get_router router_instance = get_router() if not router_instance: return {"enabled": False} return {"enabled": True, **router_instance.get_status()} @router.post("/{provider_id}/detect") async def detect_cli_token(provider_id: str): """Auto-detect CLI token for a specific provider.""" from backend.core.smart_router import get_registry, get_extractor registry = get_registry() extractor = get_extractor() if not registry or not extractor: raise HTTPException(400, "Smart Router not enabled") token = extractor.detect(provider_id) if not token: return {"detected": False, "message": f"No CLI token found for {provider_id}"} # Add to registry acct_id = registry.add_account( provider_id=provider_id, label=token.label, credential=token.token, credential_type=token.credential_type, source="cli_detect", refresh_token=token.refresh_token, expires_at=token.expires_at, ) return { "detected": True, "account_id": acct_id, "label": token.label, "credential_type": token.credential_type, "has_refresh_token": token.refresh_token is not None, "expires_at": token.expires_at, } @router.post("/{provider_id}/connect") async def connect_provider(provider_id: str, req: ConnectRequest): """Manually add an API key or credential.""" from backend.core.smart_router import get_registry registry = get_registry() if not registry: raise HTTPException(400, "Smart Router not enabled") acct_id = registry.add_account( provider_id=provider_id, label=req.label, credential=req.credential, credential_type=req.credential_type, source="manual", model_override=req.model_override, ) if not acct_id: raise HTTPException(404, f"Unknown provider: {provider_id}") return {"success": True, "account_id": acct_id} @router.delete("/{provider_id}/accounts/{account_id}") async def remove_account(provider_id: str, account_id: str): """Remove an account from a provider.""" from backend.core.smart_router import get_registry registry = get_registry() if not registry: raise HTTPException(400, "Smart Router not enabled") success = registry.remove_account(provider_id, account_id) if not success: raise HTTPException(404, "Account not found") return {"success": True} @router.post("/test/{provider_id}/{account_id}") async def test_connection(provider_id: str, account_id: str): """Test connectivity for a specific account.""" from backend.core.smart_router import get_router router_instance = get_router() if not router_instance: raise HTTPException(400, "Smart Router not enabled") success, message = await router_instance.test_account(provider_id, account_id) return {"success": success, "message": message} # Known models per provider for dropdown selection PROVIDER_MODELS = { "claude_code": [ "claude-opus-4-6-20250918", "claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-4-20250514", ], "kiro": [ "claude-opus-4-6-20250918", "claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-4-20250514", ], "anthropic": [ "claude-opus-4-6-20250918", "claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-4-20250514", "claude-3-5-sonnet-20241022", ], "codex_cli": [ "gpt-4o", "gpt-4o-mini", "o3-mini", "o4-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", ], "openai": [ "gpt-4o", "gpt-4o-mini", "o3-mini", "o4-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", ], "gemini_cli": [ "gemini-3.0-pro", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite", ], "gemini": [ "gemini-3.0-pro", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite", ], "cursor": [ "cursor-fast", "cursor-small", "gpt-4o", "claude-sonnet-4-5-20250929", "claude-3-5-sonnet-20241022", ], "copilot": [ "gpt-4o", "gpt-4o-mini", "claude-sonnet-4-5-20250929", "claude-3-5-sonnet-20241022", ], "openrouter": [ "anthropic/claude-opus-4-6", "anthropic/claude-sonnet-4-5", "anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-4", "anthropic/claude-opus-4", "openai/gpt-4o", "google/gemini-3.0-pro", "google/gemini-2.5-pro", "google/gemini-2.5-flash", "meta-llama/llama-4-maverick", "deepseek/deepseek-r1", ], "together": [ "meta-llama/Llama-3-70b-chat-hf", "meta-llama/Llama-3.3-70B-Instruct-Turbo", "deepseek-ai/DeepSeek-R1", "Qwen/Qwen2.5-72B-Instruct-Turbo", ], "fireworks": [ "accounts/fireworks/models/llama-v3p1-70b-instruct", "accounts/fireworks/models/llama-v3p3-70b-instruct", "accounts/fireworks/models/deepseek-r1", ], "iflow": ["kimi-k2"], "qwen_code": ["qwen3-coder", "qwen-max"], "ollama": ["llama3", "llama3.2", "mistral", "codellama", "deepseek-r1"], "lmstudio": ["local-model"], } @router.get("/available-models") async def available_models(): """Get list of available provider+model combinations for selection dropdowns.""" from backend.core.smart_router import get_registry registry = get_registry() if not registry: return {"models": []} models = [] for p in registry.get_all_providers(): active = registry.get_active_accounts(p.id) if not active: continue models.append({ "provider_id": p.id, "provider_name": p.name, "default_model": p.default_model, "tier": p.tier, "available_models": PROVIDER_MODELS.get(p.id, [p.default_model]), }) # Sort by tier (paid first) then name models.sort(key=lambda m: (m["tier"], m["provider_name"])) return {"models": models} @router.post("/detect-all") async def detect_all_tokens(): """Scan all CLI tools for available tokens.""" from backend.core.smart_router import get_registry, get_extractor registry = get_registry() extractor = get_extractor() if not registry or not extractor: raise HTTPException(400, "Smart Router not enabled") tokens = extractor.detect_all() results = [] for token in tokens: acct_id = registry.add_account( provider_id=token.provider_id, label=token.label, credential=token.token, credential_type=token.credential_type, source="cli_detect", refresh_token=token.refresh_token, expires_at=token.expires_at, ) results.append({ "provider_id": token.provider_id, "label": token.label, "account_id": acct_id, }) return { "detected_count": len(results), "results": results, } class ToggleRequest(BaseModel): enabled: bool @router.post("/{provider_id}/toggle") async def toggle_provider(provider_id: str, req: ToggleRequest): """Enable or disable a provider. Disabled providers are skipped by the router.""" from backend.core.smart_router import get_registry registry = get_registry() if not registry: raise HTTPException(400, "Smart Router not enabled") success = registry.toggle_provider(provider_id, req.enabled) if not success: raise HTTPException(404, f"Unknown provider: {provider_id}") return {"success": True, "provider_id": provider_id, "enabled": req.enabled} # Whitelist of env keys that can be modified via UI ALLOWED_ENV_KEYS = { "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY", "TOGETHER_API_KEY", "FIREWORKS_API_KEY", "OLLAMA_HOST", "LMSTUDIO_HOST", "ENABLE_SMART_ROUTER", "ENABLE_REASONING", "ENABLE_CVE_HUNT", "ENABLE_MULTI_AGENT", "ENABLE_RESEARCHER_AI", "NVD_API_KEY", "GITHUB_TOKEN", "TOKEN_BUDGET", } class EnvUpdateRequest(BaseModel): key: str value: str @router.get("/env") async def get_env_keys(): """Get current values of allowed env keys (masked for secrets).""" import os result = {} for key in sorted(ALLOWED_ENV_KEYS): val = os.getenv(key, "") if val and "KEY" in key and key not in ("ENABLE_SMART_ROUTER", "ENABLE_REASONING", "ENABLE_CVE_HUNT", "ENABLE_MULTI_AGENT", "ENABLE_RESEARCHER_AI", "TOKEN_BUDGET"): # Mask API keys: show first 8 and last 4 chars if len(val) > 16: result[key] = val[:8] + "..." + val[-4:] else: result[key] = "****" else: result[key] = val return {"env": result, "allowed_keys": sorted(ALLOWED_ENV_KEYS)} @router.post("/env") async def update_env_key(req: EnvUpdateRequest): """Update an env var and persist to .env file.""" import os from pathlib import Path if req.key not in ALLOWED_ENV_KEYS: raise HTTPException(400, f"Key '{req.key}' is not in the allowed whitelist") # Update in-process env os.environ[req.key] = req.value # Persist to .env file env_path = Path(__file__).parent.parent.parent.parent / ".env" try: lines = [] found = False if env_path.exists(): for line in env_path.read_text().splitlines(): stripped = line.strip() if stripped.startswith(f"{req.key}=") or stripped.startswith(f"# {req.key}="): lines.append(f"{req.key}={req.value}") found = True else: lines.append(line) if not found: lines.append(f"{req.key}={req.value}") env_path.write_text("\n".join(lines) + "\n") except Exception as e: # Still updated in-process even if file write failed return {"success": True, "persisted": False, "error": str(e)} return {"success": True, "persisted": True}