AI Provider Guide
How the four supported AI providers are wired up, what enables each one, and how to add a fifth without touching the registry, manager, or any service.
Required environment variables
See .env.local.example for the authoritative list. An app with none of these set still runs — every AI feature reports unconfigured rather than crashing (see "Unconfigured behavior" below).
| Variable | Enables | Required |
|---|---|---|
OPENAI_API_KEY | OpenAI (gpt-4o, gpt-4o-mini, text-embedding-3-small) | No |
ANTHROPIC_API_KEY | Anthropic (claude-opus-5, claude-sonnet-5, claude-haiku-4-5) | No |
GOOGLE_AI_API_KEY | Google Gemini (gemini-2.5-pro, gemini-2.5-flash, text-embedding-004) | No |
OLLAMA_BASE_URL | Ollama (local inference, default http://localhost:11434) | No (defaults if unset) |
AI_AUTOMATION_RUN_SECRET | Shared secret for GET /api/ai/automations/run (external cron) | Yes, for automations to actually fire |
At minimum, configure one cloud provider key for chat/journal/coach/ risk/market/prop-firm features to produce real output rather than AIProviderNotConfiguredError. OpenAI is also the platform's standardized embedding provider (text-embedding-3-small, 1536-dim) — RAG needs OPENAI_API_KEY regardless of which provider answers a given chat turn.
Provider catalog (src/lib/ai/registry/providers.ts)
AI_PROVIDERS is the single source of truth for every model id, context window, capability flags, and per-1M-token pricing. See docs/AI_COST_GUIDE.md for the pricing honesty note — figures are illustrative snapshots, not a live pricing feed.
| Provider | Chat/completion API | Streaming | Tool calling | Embeddings |
|---|---|---|---|---|
| OpenAI | POST /v1/chat/completions (stream: true) | SSE | ✓ | POST /v1/embeddings |
| Anthropic | POST /v1/messages (Messages API) | SSE | ✓ (tool_use blocks) | ✗ — real API limitation, embed() throws AIProviderNotImplementedError |
| Gemini | :generateContent / :streamGenerateContent | SSE | ✓ | :embedContent |
| Ollama | POST /api/chat (local) | NDJSON | ✓ | POST /api/embeddings |
Every adapter reuses fetchJson from connectors/shared/http.ts for non-streaming calls (retry/backoff for free) and ai/shared/sse.ts's streamLines() for streaming — one generic line-splitter every provider's own SSE/NDJSON parsing sits on top of. Not live-tested against a real API key in this environment (no key was available) — each adapter was built and verified against each provider's official, documented request/response shapes, the same disclosed-confidence standard as this codebase's Stripe integration (docs/BILLING.md's "Not live-tested" section).
Unconfigured behavior
src/lib/ai/register.ts registers all four providers lazily and idempotently at first use. A provider whose env var is missing is backed by StubAIProvider:
generate()/stream()/embed()throwAIProviderNotConfiguredError
immediately — a clear, typed error, never a silent no-op or a fabricated response.
healthCheck()still returns a real result:{ status: 'unconfigured' }
— this is what /admin/ai/providers shows for a provider with no key set.
Failover
AIProviderManager.generate()/.stream() accept an optional fallback chain of { provider, model } pairs (see shared/defaults.ts's DEFAULT_GENERATION_FALLBACKS). On a retryable failure (rate limit, 5xx, AIProviderNotConfiguredError) it tries the next model in the chain before raising. This is the "provider switching" requirement made concrete — no manual intervention needed to keep a feature working if one provider is down or unconfigured, as long as at least one fallback is.
Adding a 5th provider
1. Add one AIProviderId union member (sdk/types.ts). 2. Add one entry to AI_PROVIDERS (registry/providers.ts) — models, context windows, capability flags, pricing. 3. Implement the AIProvider interface in a new src/lib/ai/providers/<name>/<name>-provider.ts, extending BaseAIProvider, reusing fetchJson/streamLines. 4. Add one register<Name>Provider() function to register.ts, called from ensureAIProvidersRegistered(). 5. Nothing else changes — no service, no feature module, no UI component references a specific provider by name; every call site goes through AIProviderManager by feature + model id.
BYOK (future, not built this milestone)
Every call site resolves a provider via AIProviderRegistry.create(id), which today always closes over process.env.<PROVIDER>_API_KEY. Adding Bring-Your-Own-Key later means changing what that factory closes over (an org-supplied, encrypted key instead of the env var) at the registration call site only — the interface, manager, and every service/ feature module above it are unaffected. No BYOK UI, storage, or per-organization key resolution exists today; this is a documented seam, not a partial implementation.