LLM Provider
Classificação remota opcional, provider-agnostic via trait LlmProvider
LLM Provider
O LLM é a camada opcional de maior custo: roda como
fork para eventos que o hot path e o
modelo local não resolveram — score ≥ only_above (30) ou verdict
Quarantine. O core nunca chama uma API de LLM diretamente: tudo passa pela
trait LlmProvider,
e a seleção acontece por config. Default é desligado
([llm] provider = "none").
Trait LlmProvider
#[async_trait]
pub trait LlmProvider: Send + Sync {
fn name(&self) -> &'static str; // "openrouter", "ollama", …
fn model_id(&self) -> &str; // ex: "anthropic/claude-3.5-sonnet"
async fn classify(&self, req: ClassifyRequest) -> anyhow::Result<ClassifyResponse>;
async fn explain(&self, req: ExplainRequest) -> anyhow::Result<String>;
}Definida em
sentry-ai/src/llm.rs.
Contratos ao redor:
ClassifyRequest— o payload (ProtocolData, serve para Http/Tcp/Tls/…), um resumo humanamente legível truncado e o JSON schema obrigatório da resposta.ClassifyResponse—verdict,risk_score(0–100),risk_level,signals,confidence(0.0–1.0),explanationopcional eusage(LlmUsage: tokens de entrada/saída + custo, quando o provedor reporta).ExplainRequest— explicação free-form de um evento já classificado (CLI e webhooks).
Prompt e parsing compartilhados
Todo adapter envia o mesmo system prompt e o mesmo JSON schema
(llm/prompt.rs):
SYSTEM_PROMPT— enquadra o modelo como analista de triagem WAF e manda ser conservador: tráfego comum →allowcom score baixo; padrões de ataque conhecidos →block/challengecom score alto.classify_schema— schema estrito: enum fechado de verdicts,risk_score0–100,signals,confidence; campos extras proibidos (additionalProperties: false).context_from_event— resumo do evento para a user message (método, path, query, status, UA, referer, preview do body; SNI/JA3/JA4 para TLS), truncado a 2048 caracteres para custo de token previsível.parse_classify— parsing tolerante: extrai o objeto JSON mesmo cercado de prose ou markdown fences, normaliza tokens ("Rate-Limit"=rate_limit,"deny"=block), clampa score/confidence fora de faixa e re-derivarisk_leveldo score.
Adapters
Construídos por config em
make_llm_provider
(que também é usada pelo sentry bench llm). Nome desconhecido ou chave faltando
= warning + estágio desligado no run — nunca um crash.
Provider ([llm] provider) | Implementação | Endpoint | Auth | Notas |
|---|---|---|---|---|
openrouter (recomendado) | OpenRouterProvider | https://openrouter.ai/api/v1/chat/completions | SENTRY_LLM_KEY (bearer) — sem chave, o estágio é desabilitado | Um endpoint roteia para qualquer modelo (Claude, GPT, Gemini, Qwen, Llama, …). Envia response_format: json_schema com strict: true. Timeout 60 s. Model default: openai/gpt-4o-mini. |
ollama | OllamaProvider | http://localhost:11434/api/chat | — (local, sem chave) | Zero custo, zero vazamento de dados. Schema vai no system prompt + format: "json" (o suporte a response_format varia entre versões do Ollama). Timeout 120 s. Model default: llama3.1. |
openai | reusa OpenRouterProvider | qualquer servidor OpenAI-compatible (base_url; default http://localhost:1234/v1) | SENTRY_LLM_KEY enviado como bearer quando setado | LM Studio, vLLM, llama.cpp server — roda keyless em servidor local. |
jev | JevProvider | https://api.typesafe.ai | SENTRY_JEV_KEY (ou TYPESAFE_API_KEY / JEV_API_KEY / ~/.config/typesafe/key) | Classificador calibrado, não generativo: um call responde verdict (choice) + risco 0–9 com probabilidades — sem reparo de JSON, sem alucinação de campos. explain devolve breakdown, não prosa. Model default: jev-latest. |
mock | MockLlmProvider | — | — | Determinístico para testes e wiring (default: block, score 90, confiança 0.9). |
Não existe adapter dedicado Anthropic/OpenAI-SDK: use openrouter (que
roteia para esses modelos) ou openai com base_url apontando para um
endpoint compatível.
O fork no daemon (LlmFork)
LlmFork
espelha o fork da IA local:
- Gatilho: score ≥
only_above(default 30) ou verdict Quarantine. - Semáforo
[llm] concurrency(default 4) + cache TTL por hash do payload (cache_ttl_secs, default 300 s) — payloads idênticos não re-chamam o LLM na janela. - Mapeamento para sinais
(
llm_signals): resposta benigna (verdictallowcom score < 20) não gera sinal algum — o fork só eleva, então sinal de peso zero seria ruído. Resposta maliciosa → sinalLlmMaliciouscom peso = risk_score × confidence e detail =explanation(ou a lista de sinais). - Reentrada pelo
rescore_from(só eleva); emmode = "fork"o evento é atualizado no Postgres e as actions aplicáveis re-dispatchadas; emmode = "shadow"o resultado é apenas logado (would change verdict) — nada age.
Configuração [llm] — exemplo mínimo
[llm]
provider = "openrouter"
model = "anthropic/claude-3.5-sonnet"
mode = "fork" # ou "shadow" p/ observar sem agir
only_above = 30 # escala a partir deste score (ou em Quarantine)
concurrency = 4
cache_ttl_secs = 300export SENTRY_LLM_KEY="sk-or-v1-…"Chaves e defaults em LlmConfig;
o provider padrão é none (desligado). Para rodar 100% local, troque para
provider = "ollama" com model = "llama3.1" — nenhuma chave, nenhuma
mudança de código.
Avaliando providers
sentry bench llm --providers mock,ollama,openrouter --n 200O harness sentry bench llm
compara provedores em latência, concordância com o pipeline de heurísticas,
qualidade do output e custo — com kit sintético rotulado embutido ou
replayando um access.log (--events) — antes de você commitar dinheiro num
provider.