LLM integration¶
AlertSage routes the LLM second opinion through whichever provider you select. Four providers are supported out of the box:
| Provider | Default model | When to use |
|---|---|---|
| Hugging Face Inference | meta-llama/Llama-3.1-8B-Instruct:cerebras | Quick demo, free-tier or paid HF account, hosted demo on Streamlit Cloud. |
| OpenAI | gpt-4o-mini | Best rationale quality on commodity hardware, low cost. |
| Anthropic | claude-haiku-4-5 | Best rationale quality with longer context. |
| Local llama.cpp | local .gguf file | Air-gapped deployments, no network egress. |
All providers implement the same generate_json(prompt) -> dict interface so the dispatcher in llm_helpers.llm_second_opinion() is provider-agnostic. Adding a fifth provider is a small, isolated patch in src/triage/llm_client.py.
Bring Your Own Key¶
Keys never touch disk
API keys live in st.session_state only. They are never written to data/triage.db and never persisted to any other file. Closing the browser tab clears them.
The console exposes a Bring Your Own Key panel for each remote provider in Settings. Paste a key once, it stays for the rest of the session. The same provider also accepts the key from ~/.streamlit/secrets.toml or environment variables; precedence is sidebar/Settings > secrets.toml > env vars.
Where keys are read¶
| Provider | Sidebar/Settings | Secrets file key | Env var |
|---|---|---|---|
| Hugging Face | yes (token) | HF_TOKEN, HF_MODEL | TRIAGE_HF_TOKEN, HF_TOKEN, TRIAGE_HF_MODEL, HF_MODEL |
| OpenAI | yes (key) | not read | session-only |
| Anthropic | yes (key) | not read | session-only |
| Local | n/a | n/a | TRIAGE_LLM_MODEL (path to .gguf) |
| VirusTotal | yes (key) | VIRUSTOTAL_API_KEY, VT_API_KEY | VIRUSTOTAL_API_KEY, VT_API_KEY |
OpenAI and Anthropic keys are session only by design. If you want to persist them for a deployment, export them in your shell or pass via your hosting platform's secrets mechanism.
Fallback chain¶
The dispatcher (_build_llm_kwargs in app.py) routes intelligently when keys are missing or providers are unavailable:
Pick OpenAI:
has key? -> OpenAI
no key, hf -> Hugging Face Router (if HF_TOKEN)
no key, local -> local llama.cpp (if GGUF available)
Pick Anthropic:
has key? -> Anthropic
no key, hf -> Hugging Face Router (if HF_TOKEN)
no key, local -> local llama.cpp (if GGUF available)
Pick Local:
GGUF + llama_cpp present? -> local llama.cpp
otherwise -> Hugging Face Router
The sidebar caption shows the actually-active backend after fallbacks resolve.
Per-provider rate limits¶
Each provider has its own sliding-window limiter, default 5 requests per 60 seconds. Burning OpenAI quota does not lock out Anthropic or Hugging Face. The window and request count come from RATE_LIMIT_REQS and RATE_LIMIT_WINDOW_S in app.py.
When the limit is hit, the UI surfaces a friendly "wait Ns" message instead of a stack trace.
Hugging Face Inference¶
Default provider for the hosted demo. Uses the HF Router REST endpoint (https://router.huggingface.co/v1/chat/completions), so you can target any model the router supports including provider-suffixed variants like meta-llama/Llama-3.1-8B-Instruct:cerebras.
# Read once and cache
export HF_TOKEN="hf_..."
# Or via secrets.toml
cat > .streamlit/secrets.toml <<EOF
HF_TOKEN = "hf_..."
HF_MODEL = "meta-llama/Llama-3.1-8B-Instruct:cerebras"
EOF
Token has Inference scope (free-tier works for low-volume demos).
OpenAI¶
Uses the official openai Python SDK with the Chat Completions endpoint and response_format={"type": "json_object"} to enforce JSON output.
# What the dispatcher calls under the hood
client = OpenAIClient(
api_key="sk-...",
model="gpt-4o-mini",
max_new_tokens=512,
rate_limiter=...,
)
response = client.generate_json(prompt) # returns dict
Recommended models:
gpt-4o-mini(default): cheap, fast, good enough for SOC rationalegpt-4o: better quality, more expensivegpt-4.1-mini: middle ground
The system prompt (SOC_SYSTEM_PROMPT in llm_client.py) instructs the model to return strict JSON with label, mitre_ids, and rationale.
Anthropic¶
Uses the official anthropic Python SDK with the Messages API. The system prompt is set via system=, the user prompt via messages=[{"role": "user", "content": ...}].
Recommended models:
claude-haiku-4-5(default): cheap, fast, longer context than gpt-4o-miniclaude-sonnet-4-6: higher qualityclaude-opus-4-7: highest quality, most expensive
Anthropic responses come back as a list of content blocks; the client extracts block.text for block.type == "text" and concatenates.
Local llama.cpp¶
For air-gapped or zero-egress deployments. The Local (GGUF) provider option in the sidebar is automatically hidden when its prerequisites are missing:
- The
llama_cppPython package must be importable. - A
.gguffile must be present inmodels/.
When either check fails, the option does not appear and the dispatcher's fallback routes to Hugging Face instead.
Install llama-cpp-python¶
# Apple Silicon (Metal acceleration)
CMAKE_ARGS="-DLLAMA_METAL=on" pip install llama-cpp-python
# CUDA
CMAKE_ARGS="-DLLAMA_CUDA=on" pip install llama-cpp-python
# CPU only
pip install llama-cpp-python
Or use the dev extras: pip install -e ".[dev]" includes a pinned llama-cpp-python==0.3.16.
Download a GGUF model¶
mkdir -p models
# Llama 3.1 8B Instruct, Q6_K (about 6 GB, recommended)
huggingface-cli download TheBloke/Llama-3.1-8B-Instruct-GGUF \
Llama-3.1-8B-Instruct-Q6_K.gguf --local-dir models
# Mistral 7B Instruct v0.2, Q6_K (about 5.5 GB)
huggingface-cli download TheBloke/Mistral-7B-Instruct-v0.2-GGUF \
mistral-7b-instruct-v0.2.Q6_K.gguf --local-dir models
# TinyLlama 1.1B, Q4_K_M (about 700 MB, CPU-friendly)
huggingface-cli download TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF \
tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf --local-dir models
Configure the path¶
Default path: models/Meta-Llama-3.1-8B-Instruct-Q6_K.gguf. Override with TRIAGE_LLM_MODEL:
GPU acceleration is auto-enabled at first call:
LLAMA_N_GPU_LAYERS=999(offload all layers)LLAMA_METAL=1on macOSLLAMA_CUDA=1on NVIDIA
To force CPU-only, set these to 0 in your environment before launching.
VirusTotal IOC enrichment¶
The IOC panel on Investigate auto-extracts indicators from the narrative (IPv4, IPv6, MD5, SHA1, SHA256, URL, email, domain, CVE, hostnames). Without a VirusTotal key it shows a deterministic mock: the same indicator always returns the same verdict, score, and source list, so the demo is reproducible.
Paste a VirusTotal API key in Settings -> Threat intel enrichment and the panel switches to live VT lookups. Free-tier keys work; results are cached for 15 minutes per indicator to stay under the 4 req/min free-tier limit.
The enrichment shape is provider-agnostic so swapping in AbuseIPDB, OTX, GreyNoise, or an aggregator is a one-function change. See _vt_lookup and _enrich_ioc_real_or_mock in app.py.
External pivot links¶
Each IOC row also exposes click-through pivot links to:
| IOC type | Pivots |
|---|---|
| ipv4 / ipv6 | VirusTotal, AbuseIPDB, Shodan, GreyNoise |
| domain | VirusTotal, URLhaus, Censys |
| md5 / sha1 / sha256 | VirusTotal, MalwareBazaar |
| url | URLhaus, VirusTotal |
| cve | NVD, MITRE CVE |
These open in a new tab and require no API keys.
Prompt engineering¶
The triage prompt is a single function (llm_second_opinion in src/triage/llm_helpers.py). Two key elements:
- Strict JSON output with three keys:
label,mitre_ids,rationale. The system prompt explicitly tells the model to refuse free-form prose and only emit the JSON object. - Hallucination guardrails: extracted IOCs from the narrative are compared against the model's rationale. If the model invents an IOC that wasn't in the source narrative, the label is downgraded to
uncertainand the rationale is replaced with a deterministic fallback. See_extract_indicatorsinllm_helpers.py.
A second pass applies keyword validation per label. If the model returns data_exfiltration but the narrative contains no exfil-style keywords, the label downgrades to uncertain. This is a deliberate conservatism trade-off: better to mark uncertain than to confidently misclassify.
Debugging¶
Enable verbose LLM logs by setting TRIAGE_LLM_DEBUG=1 before launching:
Debug output goes to stderr (so JSON output on stdout from the CLI stays clean).
The Settings -> Demo data generator panel also surfaces the last LLM error if the demo fragment hits one. Use this to diagnose key issues, model mismatches, or rate-limit blowback without digging through logs.
Cost notes¶
The LLM second opinion runs once per triage. With a 200-row Batch and OpenAI gpt-4o-mini at typical pricing, a full run is fractions of a cent. Anthropic Haiku is similar. Hugging Face free-tier covers small demos; paid is per-token.
For larger deployments, set the per-provider rate limit and watch the sidebar's rate-limit caption. Or skip the LLM entirely by leaving the LLM second opinion checkbox off in the sidebar; the classifier alone still produces a label, MITRE mapping, and a deterministic rule-based rationale.