diff --git a/CHANGELOG.md b/CHANGELOG.md index a042be92..ce3f4ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added +- **Atlas Cloud provider** (`atlascloud`) for standalone `bughunter` / `brain.py`: OpenAI-compatible gateway at `https://api.atlascloud.ai/v1` via `ATLASCLOUD_API_KEY` (optional `ATLASCLOUD_BASE_URL`). Not in the auto-detect priority list; choose option 12 in `bughunter setup`, `BRAIN_PROVIDER=atlascloud`, or `--provider atlascloud`. Default model `deepseek-ai/deepseek-v4-flash` and a short curated model list (same pattern as Requesty). - **Requesty provider** (`requesty`) for standalone `bughunter` / `brain.py`: OpenAI-compatible gateway at `https://router.requesty.ai/v1` via `REQUESTY_API_KEY` (optional `REQUESTY_BASE_URL`, e.g. `https://router.eu.requesty.ai/v1` for the EU region). Not in the auto-detect priority list; choose option 11 in `bughunter setup`, `BRAIN_PROVIDER=requesty`, or `--provider requesty`. Default model `anthropic/claude-sonnet-4-6` and a short curated model list (same pattern as OpenRouter). - **Fluxion AI provider** (`fluxion`) for standalone `bughunter` / `brain.py` — optional OpenAI-compatible gateway at `https://fluxionai.world/v1` via `FLUXION_API_KEY`. Not the default; choose option 9 in `bughunter setup`, `BRAIN_PROVIDER=fluxion`, or `--provider fluxion`. README partner placement + setup docs link [docs](https://docs.fluxionai.world/user-guide/help-center) and [Model Plaza](https://fluxionai.world/model-plaza). - **Claude Code plugin manifest** (`.claude-plugin/plugin.json`) — declares skills, commands, agents, and hooks so the repo passes Claude Code / marketplace plugin validation. diff --git a/README.md b/README.md index b1615cda..beed7aff 100644 --- a/README.md +++ b/README.md @@ -213,8 +213,9 @@ bughunter v "finding" # short alias for validate | **Sidrune AI** | Subscription / pay-as-you-go | Cloud | Fast | [sidrune.ai](https://fluxionai.space/register?source=github&campaign=github-sidrune-agentic-bug-hunter&promo=SDRBUGHUNTER) → get API key · [docs](https://docs.fluxionai.world/user-guide/help-center) · [Model Plaza](https://fluxionai.world/model-plaza) | | **LiteLLM** | Uses your existing provider keys | Cloud / self-hosted proxy | Fast | [docs.litellm.ai](https://docs.litellm.ai) → one gateway for 100+ models | | **Requesty** | Pay-as-you-go | Cloud | Fast | [app.requesty.ai/api-keys](https://app.requesty.ai/api-keys) → get API key · [docs](https://docs.requesty.ai) | +| **Atlas Cloud** | Pay-as-you-go | Cloud | Fast | [console.atlascloud.ai/api-keys](https://console.atlascloud.ai/api-keys) → get API key · [docs](https://docs.atlascloud.ai) | -BugHunter auto-detects providers in this order: **Ollama → Groq → DeepSeek → … → OrcaRouter → OpenRouter → Sidrune AI → Claude → OpenAI**. LiteLLM is opt-in (selected explicitly or when `LITELLM_API_KEY` is set) so it never preempts a provider you already configured. Requesty is opt-in the same way (selected explicitly or when `REQUESTY_API_KEY` is set); set `REQUESTY_BASE_URL=https://router.eu.requesty.ai/v1` to route through the EU region. +BugHunter auto-detects providers in this order: **Ollama → Groq → DeepSeek → … → OrcaRouter → OpenRouter → Sidrune AI → Claude → OpenAI**. LiteLLM is opt-in (selected explicitly or when `LITELLM_API_KEY` is set) so it never preempts a provider you already configured. Requesty and Atlas Cloud are opt-in the same way (selected explicitly or when `REQUESTY_API_KEY` / `ATLASCLOUD_API_KEY` is set); set `REQUESTY_BASE_URL=https://router.eu.requesty.ai/v1` to route through the EU region. Switch providers or choose an installed Ollama model anytime: `bughunter setup`. The setup can also be fully non-interactive: diff --git a/bughunter/brain.py b/bughunter/brain.py index c983d302..34c71528 100644 --- a/bughunter/brain.py +++ b/bughunter/brain.py @@ -5,13 +5,13 @@ Brain — Multi-Provider LLM Reasoning Layer for Bug Bounty & VAPT Supports: Ollama (local), Claude, OpenAI, Grok, Groq, DeepSeek, Gemini, Kimi (Moonshot), Mistral, Together AI, Cerebras, Perplexity, - OpenRouter, OrcaRouter, Fluxion, Requesty + OpenRouter, OrcaRouter, Fluxion, Requesty, Atlas Cloud Provider selection (in order of precedence): 1. BRAIN_PROVIDER env var (ollama | claude | openai | grok | groq | deepseek | gemini | kimi | mistral | together | cerebras | perplexity | openrouter | orcarouter | fluxion | - requesty) + requesty | atlascloud) 2. Auto-detect: uses first provider whose API key / server is available Model selection: @@ -38,6 +38,8 @@ REQUESTY_API_KEY - Requesty (multi-model gateway, anthropic/claude-sonnet-4-6, etc.) REQUESTY_BASE_URL - Requesty base URL (default: https://router.requesty.ai/v1, EU: https://router.eu.requesty.ai/v1) + ATLASCLOUD_API_KEY - Atlas Cloud (multi-model gateway, deepseek-ai/deepseek-v4-flash, etc.) + ATLASCLOUD_BASE_URL - Atlas Cloud base URL (default: https://api.atlascloud.ai/v1) OLLAMA_HOST — Ollama base URL (default: http://localhost:11434) Default model priority (uses first available): @@ -209,6 +211,7 @@ class LLMClient: "orcarouter": "openai/gpt-4o", "fluxion": "openai/gpt-4o", "requesty": "anthropic/claude-sonnet-4-6", + "atlascloud": "deepseek-ai/deepseek-v4-flash", "litellm": "gpt-4o", "ollama": None, # resolved dynamically } @@ -278,6 +281,7 @@ def __init__(self, provider: str | None = None, model: str | None = None): "orcarouter": "ORCAROUTER_API_KEY", "fluxion": "FLUXION_API_KEY", "requesty": "REQUESTY_API_KEY", + "atlascloud": "ATLASCLOUD_API_KEY", # Kept last so auto-detect only reaches LiteLLM when LITELLM_API_KEY is # set, and never preempts a directly-configured provider above. "litellm": "LITELLM_API_KEY", @@ -526,6 +530,21 @@ def _init_provider(self, provider: str) -> None: self.available = True self.description = "Requesty (multi-model gateway)" + elif provider == "atlascloud": + key = os.environ.get("ATLASCLOUD_API_KEY", "") + if not key: + return + import requests + self._http = requests.Session() + self._http.headers.update({ + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + }) + self._api_base = (os.environ.get("ATLASCLOUD_BASE_URL", "") + or "https://api.atlascloud.ai/v1").rstrip("/") + self.available = True + self.description = "Atlas Cloud (multi-model gateway)" + elif provider == "litellm": # LiteLLM routes by the model-name prefix (e.g. "anthropic/claude-...", # "gemini/gemini-...", "bedrock/...") and reads each target provider's @@ -557,7 +576,7 @@ def chat(self, model: str | None, system: str, user: str, elif self.provider in ( "openai", "grok", "groq", "deepseek", "gemini", "kimi", "mistral", "together", "cerebras", "perplexity", - "openrouter", "orcarouter", "fluxion", "requesty", + "openrouter", "orcarouter", "fluxion", "requesty", "atlascloud", ): return self._chat_openai_compat(model, system, user, max_tokens, temperature) except Exception as e: @@ -753,6 +772,18 @@ def list_models(self) -> list[str]: "gemini-3.5-flash", "gpt-4o-mini@eu", ] + elif self.provider == "atlascloud": + # Curated starter set; full catalog: GET https://api.atlascloud.ai/v1/models + return [ + "deepseek-ai/deepseek-v4-flash", + "deepseek-ai/deepseek-v4-pro", + "zai-org/glm-5.2", + "moonshotai/kimi-k3", + "qwen/qwen3.5-plus", + "openai/gpt-5.5", + "anthropic/claude-sonnet-4.6", + "google/gemini-3.1-pro-preview", + ] elif self.provider == "litellm": return self._litellm_list_models() return [] diff --git a/bughunter/engine.py b/bughunter/engine.py index 2d1bbf44..251f0fc4 100755 --- a/bughunter/engine.py +++ b/bughunter/engine.py @@ -28,6 +28,9 @@ requesty - multi-model gateway, set REQUESTY_API_KEY get key: https://app.requesty.ai/api-keys docs: https://docs.requesty.ai + atlascloud - multi-model gateway, set ATLASCLOUD_API_KEY + get key: https://console.atlascloud.ai/api-keys + docs: https://docs.atlascloud.ai Usage: ./engine.py setup one-time config wizard @@ -272,6 +275,7 @@ def cmd_setup(args): "9": ("fluxion", "Fluxion (multi-model) — needs FLUXION_API_KEY"), "10": ("litellm", "LiteLLM (100+ providers) — uses per-provider keys or LITELLM_API_KEY"), "11": ("requesty", "Requesty (multi-model) - needs REQUESTY_API_KEY"), + "12": ("atlascloud", "Atlas Cloud (multi-model) - needs ATLASCLOUD_API_KEY"), } requested_provider = ( @@ -308,6 +312,7 @@ def cmd_setup(args): "orcarouter": "ORCAROUTER_API_KEY", "fluxion": "FLUXION_API_KEY", "requesty": "REQUESTY_API_KEY", + "atlascloud": "ATLASCLOUD_API_KEY", } if provider in env_map: @@ -411,6 +416,7 @@ def cmd_providers(args): "orcarouter": "ORCAROUTER_API_KEY", "fluxion": "FLUXION_API_KEY", "requesty": "REQUESTY_API_KEY", + "atlascloud": "ATLASCLOUD_API_KEY", } tier = { "ollama": "FREE (local)", "groq": "FREE tier", @@ -420,6 +426,7 @@ def cmd_providers(args): "orcarouter": "subscription", "fluxion": "subscription", "requesty": "pay-as-you-go", + "atlascloud": "pay-as-you-go", } print(f"\n {'PROVIDER':<12} {'TIER':<16} {'STATUS':<20} {'NOTE'}") @@ -795,7 +802,7 @@ def main(): """), ) parser.add_argument("--provider", "-p", - help="Force provider: ollama / groq / deepseek / claude / openai / grok / openrouter / orcarouter / fluxion / litellm / requesty") + help="Force provider: ollama / groq / deepseek / claude / openai / grok / openrouter / orcarouter / fluxion / litellm / requesty / atlascloud") parser.add_argument("--model", "-m", help="Force model for this invocation (for example qwen3:14b)") parser.add_argument("--no-banner", action="store_true", help="Suppress banner") @@ -805,7 +812,7 @@ def main(): p_setup.add_argument( "--provider", dest="setup_provider", choices=["ollama", "groq", "deepseek", "claude", "openai", "grok", - "openrouter", "orcarouter", "fluxion", "litellm", "requesty"], + "openrouter", "orcarouter", "fluxion", "litellm", "requesty", "atlascloud"], help="Provider to persist (skips the provider prompt)", ) p_setup.add_argument( @@ -858,7 +865,8 @@ def main(): cfg = load_config() for env_var in ("GROQ_API_KEY", "DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "XAI_API_KEY", "OPENROUTER_API_KEY", - "ORCAROUTER_API_KEY", "FLUXION_API_KEY", "REQUESTY_API_KEY"): + "ORCAROUTER_API_KEY", "FLUXION_API_KEY", "REQUESTY_API_KEY", + "ATLASCLOUD_API_KEY"): if not os.environ.get(env_var) and cfg.get(env_var): os.environ[env_var] = cfg[env_var] diff --git a/config.example.json b/config.example.json index 891378c1..a95f89e2 100644 --- a/config.example.json +++ b/config.example.json @@ -30,6 +30,7 @@ "openrouter": "OPENROUTER_API_KEY=... (anthropic/claude-sonnet-4.6 / openai/gpt-4o / …)", "orcarouter": "ORCAROUTER_API_KEY=... (openai/gpt-4o / orcarouter/auto / …)", "fluxion": "FLUXION_API_KEY=... (openai/gpt-4o / … — https://fluxionai.world/v1)", - "requesty": "REQUESTY_API_KEY=... (anthropic/claude-sonnet-4-6 / openai/gpt-4o-mini / ...)" + "requesty": "REQUESTY_API_KEY=... (anthropic/claude-sonnet-4-6 / openai/gpt-4o-mini / ...)", + "atlascloud": "ATLASCLOUD_API_KEY=... (deepseek-ai/deepseek-v4-flash / moonshotai/kimi-k3 / ...)" } } diff --git a/tests/test_brain_auto_detect.py b/tests/test_brain_auto_detect.py index 0970d6f4..fe497d59 100644 --- a/tests/test_brain_auto_detect.py +++ b/tests/test_brain_auto_detect.py @@ -21,6 +21,7 @@ def brain_module(monkeypatch): "BRAIN_PROVIDER", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "XAI_API_KEY", "OPENROUTER_API_KEY", "ORCAROUTER_API_KEY", "FLUXION_API_KEY", "REQUESTY_API_KEY", "REQUESTY_BASE_URL", + "ATLASCLOUD_API_KEY", "ATLASCLOUD_BASE_URL", ): monkeypatch.delenv(env, raising=False) import brain @@ -245,3 +246,63 @@ def test_requesty_base_url_override(brain_module, monkeypatch): def test_requesty_is_opt_in_only(brain_module): assert "requesty" not in brain_module.LLMClient.PROVIDER_PRIORITY + + +def test_atlascloud_key_jumps_to_front(brain_module, monkeypatch): + monkeypatch.setenv("ATLASCLOUD_API_KEY", "apikey-test") + client = brain_module.LLMClient.__new__(brain_module.LLMClient) + client.available = False + tracker = _Tracker(available_provider="atlascloud") + tracker.bind(client) + + chosen = brain_module.LLMClient._auto_detect(client) + + assert chosen == "atlascloud" + assert tracker.calls[0] == "atlascloud" + + +def test_atlascloud_init_sets_api_base(brain_module, monkeypatch): + monkeypatch.setenv("ATLASCLOUD_API_KEY", "apikey-test") + client = brain_module.LLMClient.__new__(brain_module.LLMClient) + client.available = False + client._ollama = None + client._http = None + client.description = "" + client.provider = "atlascloud" + + brain_module.LLMClient._init_provider(client, "atlascloud") + + assert client.available is True + assert client._api_base == "https://api.atlascloud.ai/v1" + assert client._http.headers["Authorization"] == "Bearer apikey-test" + assert "atlas cloud" in client.description.lower() + assert brain_module.LLMClient.DEFAULT_MODELS["atlascloud"] == "deepseek-ai/deepseek-v4-flash" + assert "deepseek-ai/deepseek-v4-flash" in brain_module.LLMClient.list_models(client) + + +def test_atlascloud_base_url_override(brain_module, monkeypatch): + monkeypatch.setenv("ATLASCLOUD_API_KEY", "apikey-test") + monkeypatch.setenv("ATLASCLOUD_BASE_URL", "https://atlas.example.test/v1/") + client = brain_module.LLMClient.__new__(brain_module.LLMClient) + client.available = False + client._http = None + client.description = "" + + brain_module.LLMClient._init_provider(client, "atlascloud") + + assert client._api_base == "https://atlas.example.test/v1" + + +def test_atlascloud_without_key_is_unavailable(brain_module): + client = brain_module.LLMClient.__new__(brain_module.LLMClient) + client.available = False + client._http = None + client.description = "" + + brain_module.LLMClient._init_provider(client, "atlascloud") + + assert client.available is False + + +def test_atlascloud_is_opt_in_only(brain_module): + assert "atlascloud" not in brain_module.LLMClient.PROVIDER_PRIORITY