Files
NeuroSploit/neurosploit-rs/crates/harness/src/models.rs
T
CyberSecurityUP 56d3f0c723 NeuroSploit v3.4.0 — Rust multi-model harness + Axum dashboard
New cargo workspace `neurosploit-rs/` (single `neurosploit` binary):

harness crate:
- models.rs: 11 OpenAI-compatible providers / 31 models (Claude, GPT, Grok,
  NVIDIA NIM, DeepSeek, Mistral, Qwen, Groq, Together, OpenRouter, Ollama)
- pool.rs: ModelPool with bounded concurrency, provider failover, and N-model
  validator voting (the panel doubles as the jury)
- agents.rs: loads the existing agents_md/ library (213 agents)
- pipeline.rs: recon → parallel exploit (semaphore-bounded) → N-model
  adversarial vote → score; streams live progress over a channel
- report.rs: HTML report
- tokio + reqwest(rustls); offline mode runs the pipeline without API keys

app binary:
- clap CLI: serve | run | agents | models  (run supports --model x N, --vote-n,
  --max-agents, --offline)
- axum web dashboard with multi-model panel, live console, findings, agent
  browser, embedded report; single binary serves the SPA (no npm/build)

Verified: cargo build clean; agents/models/offline-run CLI; server endpoints
(/api/info, /api/run lifecycle, /report); dashboard + live run in Playwright.

Docs: README v3.4.0 callout + RELEASE.md notes. target/ gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:58:43 -03:00

135 lines
5.9 KiB
Rust

use anyhow::{anyhow, Result};
use serde::Serialize;
use std::time::Duration;
/// A model provider exposing an OpenAI-compatible `/chat/completions` endpoint.
#[derive(Clone, Debug, Serialize)]
pub struct Provider {
pub key: &'static str,
pub label: &'static str,
pub base_url: &'static str,
pub env_key: &'static str,
/// "cli" (also drivable by an agentic CLI) | "api"
pub kind: &'static str,
pub models: Vec<&'static str>,
}
/// The full provider registry. Every entry speaks the OpenAI chat schema
/// (Anthropic, xAI, NVIDIA NIM, DeepSeek, Mistral, Qwen, Groq, Together,
/// OpenRouter, Gemini-compat, Ollama).
pub fn providers() -> Vec<Provider> {
vec![
Provider { key: "anthropic", label: "Anthropic Claude", base_url: "https://api.anthropic.com/v1", env_key: "ANTHROPIC_API_KEY", kind: "cli",
models: vec!["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"] },
Provider { key: "openai", label: "OpenAI", base_url: "https://api.openai.com/v1", env_key: "OPENAI_API_KEY", kind: "cli",
models: vec!["gpt-5.1", "o4"] },
Provider { key: "xai", label: "xAI Grok", base_url: "https://api.x.ai/v1", env_key: "XAI_API_KEY", kind: "cli",
models: vec!["grok-4", "grok-4-fast"] },
Provider { key: "nvidia_nim", label: "NVIDIA NIM", base_url: "https://integrate.api.nvidia.com/v1", env_key: "NVIDIA_NIM_API_KEY", kind: "api",
models: vec!["nvidia/llama-3.3-nemotron-super-49b-v1", "deepseek-ai/deepseek-r1", "qwen/qwen2.5-coder-32b-instruct"] },
Provider { key: "deepseek", label: "DeepSeek", base_url: "https://api.deepseek.com/v1", env_key: "DEEPSEEK_API_KEY", kind: "api",
models: vec!["deepseek-reasoner", "deepseek-chat"] },
Provider { key: "mistral", label: "Mistral", base_url: "https://api.mistral.ai/v1", env_key: "MISTRAL_API_KEY", kind: "api",
models: vec!["mistral-large-latest", "codestral-latest"] },
Provider { key: "qwen", label: "Qwen (DashScope)", base_url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", env_key: "DASHSCOPE_API_KEY", kind: "api",
models: vec!["qwen-max", "qwen2.5-coder-32b-instruct", "qwq-plus"] },
Provider { key: "groq", label: "Groq", base_url: "https://api.groq.com/openai/v1", env_key: "GROQ_API_KEY", kind: "api",
models: vec!["llama-3.3-70b-versatile", "qwen-2.5-coder-32b"] },
Provider { key: "together", label: "Together AI", base_url: "https://api.together.xyz/v1", env_key: "TOGETHER_API_KEY", kind: "api",
models: vec!["Qwen/Qwen2.5-Coder-32B-Instruct", "deepseek-ai/DeepSeek-R1", "meta-llama/Llama-3.3-70B-Instruct-Turbo"] },
Provider { key: "openrouter", label: "OpenRouter", base_url: "https://openrouter.ai/api/v1", env_key: "OPENROUTER_API_KEY", kind: "api",
models: vec!["anthropic/claude-opus-4-8", "qwen/qwen-2.5-coder-32b-instruct", "deepseek/deepseek-r1", "meta-llama/llama-3.3-70b-instruct"] },
Provider { key: "ollama", label: "Ollama (local)", base_url: "http://localhost:11434/v1", env_key: "OLLAMA_API_KEY", kind: "api",
models: vec!["qwen2.5-coder:32b", "qwq:32b", "deepseek-r1:32b", "llama3.3:70b"] },
]
}
pub fn provider_for(key: &str) -> Option<Provider> {
providers().into_iter().find(|p| p.key == key)
}
/// A `provider:model` selection.
#[derive(Clone, Debug)]
pub struct ModelRef {
pub provider: String,
pub model: String,
}
impl ModelRef {
pub fn parse(s: &str) -> ModelRef {
match s.split_once(':') {
Some((p, m)) => ModelRef { provider: p.to_string(), model: m.to_string() },
None => ModelRef { provider: "anthropic".into(), model: s.to_string() },
}
}
pub fn label(&self) -> String {
format!("{}:{}", self.provider, self.model)
}
}
/// OpenAI-compatible chat client shared across the model pool.
#[derive(Clone)]
pub struct ChatClient {
http: reqwest::Client,
}
impl ChatClient {
pub fn new() -> Self {
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(120))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
ChatClient { http }
}
/// One chat completion. Errors (missing key, network, non-2xx) propagate so
/// the pool can fail over to the next candidate model.
pub async fn chat(&self, m: &ModelRef, system: &str, user: &str) -> Result<String> {
let p = provider_for(&m.provider)
.ok_or_else(|| anyhow!("unknown provider '{}'", m.provider))?;
let key = std::env::var(p.env_key).unwrap_or_default();
if key.is_empty() && p.key != "ollama" {
return Err(anyhow!("no API key ({}) for provider '{}'", p.env_key, p.key));
}
let url = format!("{}/chat/completions", p.base_url.trim_end_matches('/'));
let body = serde_json::json!({
"model": m.model,
"max_tokens": 4096,
"temperature": 0.2,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user}
]
});
let mut req = self.http.post(&url).json(&body);
if !key.is_empty() {
req = req.bearer_auth(&key);
}
let resp = req.send().await?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if !status.is_success() {
return Err(anyhow!("{} returned {}: {}", p.key, status, truncate(&text, 200)));
}
let v: serde_json::Value = serde_json::from_str(&text)?;
let content = v["choices"][0]["message"]["content"]
.as_str()
.ok_or_else(|| anyhow!("no content in response"))?;
Ok(content.to_string())
}
}
impl Default for ChatClient {
fn default() -> Self {
Self::new()
}
}
fn truncate(s: &str, n: usize) -> String {
if s.len() <= n {
s.to_string()
} else {
format!("{}…", &s[..n])
}
}