AI Provider Configuration
AVA's AI layer is provider-agnostic by design. Every AI call in the product runs through the Vercel AI SDK behind a small factory, so the models — and the vendor behind them — are a configuration choice, not a code fork. You can run AVA on Anthropic (the default), OpenAI, Google Gemini, or OpenRouter, switch between them with a one-line env change and a restart, and even mix two providers in the same deployment.
This matters for an MSP in three practical ways:
- No vendor lock-in. If your pricing, compliance, or data-residency requirements change, you change an env string — not the product.
- Cost control. The heavy-traffic AI features run on a separate "fast" model slot from the "quality" slot, so you can pair a cheap fast model with a stronger quality model and tune spend independently.
- One key, not four. You only need an API key for the provider(s) you actually configure — the container validates exactly that at boot.
The two model roles
AVA never names a model in code. It asks for one of two logical roles, each resolved from env at call time:
| Role | Env var | Default | What runs on it |
|---|---|---|---|
| Fast | LLM_FAST | anthropic/claude-haiku-4-5-20251001 | AI triage of every new chat (priority + issue classification + summary), similar-resolution suggestions, trend summaries, wrap-up field summarization, and the engineer console's /draft, /rewrite and /summarize commands. |
| Quality | LLM_QUALITY | anthropic/claude-sonnet-4-6 | Long-form transcript summarization — the higher-stakes prose path. |
The fast role is where nearly all of AVA's day-to-day AI traffic happens — it fires on every incoming chat (triage) and on several engineer-dashboard features — so the fast model choice dominates your AI cost. The quality role backs the transcript summarizer; note that in the shipped close flow the ticket notes are engineer-authored and the full chat transcript is posted to AutoTask verbatim, so quality-tier traffic is minimal in practice.
Call sites, precisely
| Feature | Role | Shape |
|---|---|---|
| New-chat triage (priority P1–P4, AutoTask issue/sub-issue mapping from your live picklists, topic summary) | fast | Structured output (generateObject + schema validation — no brittle JSON parsing of free text). |
| Similar-resolution suggestions (surfaces how past tickets like this one were resolved) | fast | Structured output. |
| Trend summaries (analytics dashboard) | fast | Text generation. |
| Wrap-up field summarization (condensing a field in the engineer's close form; "Draft all" runs it for all three fields) | fast | Text generation. |
Engineer /draft and /rewrite (a suggested reply the engineer reviews before sending) | fast | Text generation from the client-visible transcript only. |
Engineer /summarize (a private recap card: issue, what was tried, current state, next step, open questions) | fast | Structured output. |
| Transcript summarization | quality | Text generation, transcript capped at ~24k chars (most recent messages kept). |
Engineer AI calls are budgeted per chat and per engineer-hour (AI_MAX_CALLS_PER_SESSION, AI_MAX_CALLS_PER_ENGINEER_PER_HOUR — see configuration.md), and each one writes an [ai-usage] log line with token counts (never prompt or output text), so you can track spend from your logs.
The provider/model string format
provider/model string formatBoth LLM_FAST and LLM_QUALITY take a single string of the form:
<provider>/<model-id>
The string is split on the first slash only: everything before it selects the provider adapter, everything after it is passed to that provider verbatim as the model ID. That means model IDs that themselves contain slashes work naturally — which is exactly what OpenRouter needs:
# Anthropic native (default)
LLM_FAST=anthropic/claude-haiku-4-5-20251001
# OpenAI
LLM_FAST=openai/gpt-4o-mini
# Google Gemini
LLM_FAST=google/gemini-2.0-flash
# OpenRouter — note the model ID itself contains a slash; that's fine
LLM_FAST=openrouter/anthropic/claude-haiku-4-5A value with no slash, or an empty provider/model half, is rejected as invalid at call time. Unset or blank values fall back to the defaults above.
Provider matrix
| Provider prefix | Backend | API key env var | Notes |
|---|---|---|---|
anthropic | Anthropic API (native adapter) | ANTHROPIC_API_KEY | The default for both roles. |
openai | OpenAI API | OPENAI_API_KEY | |
google | Google Generative AI (Gemini) | GOOGLE_GENERATIVE_AI_API_KEY | |
openrouter | OpenRouter (chat completions) | OPENROUTER_API_KEY | One key, hundreds of models across vendors — useful for evaluation or for models AVA has no native adapter for. |
All AI calls are server-side only — no AI key is ever exposed to the browser or baked into the client bundle.
Conditional key requirements — validated at boot
You do not set all four keys. The container's startup validation reads your LLM_FAST / LLM_QUALITY values (applying the same defaults the app uses), derives which provider(s) they reference, and requires exactly those providers' keys — nothing more. A missing required key aborts startup with an error that names both the missing key and the tier that demanded it, e.g.:
ERROR: Missing OPENAI_API_KEY in /app/.env.local, required by: LLM_FAST=openai/gpt-4o-mini
An unrecognized provider prefix (a typo, or a provider AVA doesn't support) does not block startup — the entrypoint warns and skips the key check, and the app raises a clear error at the first AI call instead. Watch the boot log for that warning after changing these vars.
Mixing providers
The two roles are fully independent — a common pattern is a cheap high-throughput model for LLM_FAST and a stronger model for LLM_QUALITY:
LLM_FAST=google/gemini-2.0-flash
LLM_QUALITY=anthropic/claude-sonnet-4-6Set both providers' keys in that case (GOOGLE_GENERATIVE_AI_API_KEY and ANTHROPIC_API_KEY here); the boot check enforces exactly that pair.
Timeouts
| Variable | Type | Default | Effect |
|---|---|---|---|
ANTHROPIC_TIMEOUT_MS | int (ms) | 15000 | Per-request timeout applied to every AI call. |
Naming caveat: despite the ANTHROPIC_ prefix, this timeout applies to all providers — it predates the multi-provider layer and the name was kept for backward compatibility. There is no per-provider timeout var; this one value governs OpenAI, Gemini, and OpenRouter calls too.
Failure behavior — AI never blocks the chat
Every AI call site has a non-AI fallback, so a provider outage, a timed-out request, or malformed model output degrades features rather than breaking the product:
- Triage failure → the chat still starts and the AutoTask ticket is still created, with a safe default classification (normal priority, the raw client description as the summary).
- Similar-resolutions / trends / field-summarization failure → the dashboard feature returns a structured error or empty state; chat flow is untouched.
- Transcript summarization failure → the raw transcript is returned instead of a summary.
- Engineer helper failure (
/draft,/rewrite,/summarize, "Draft all") → the engineer sees a short message and can try again; nothing is sent to the client and the chat carries on.
Triage additionally uses schema-validated structured output, so a model that returns malformed JSON is caught and routed down the same fallback path — no partial or corrupted classifications reach AutoTask.
Tuning triage for your clientele
The triage prompt includes a configurable one-line description of who your clients are (COMPANY_PROFILE, default: "a managed service provider (MSP) supporting business clients"). Setting it to something specific — e.g. an MSP serving medical and dental practices — measurably sharpens issue classification for your domain. See white-label.md for this and the other identity knobs.
See also
configuration.md— the complete env-var reference.white-label.md— branding, includingCOMPANY_PROFILE.quickstart.md— bring-up order, including where the AI key fits.
Updated 15 days ago