AI Providers Reference
SABLE calls three AI providers: Anthropic (Claude, for generation, extraction, chat, and vision OCR), Voyage AI (embeddings and reranking for retrieval), and Exa (explicit public-web research, opt-in and lead/admin-gated). This page is the model-role map and the cost model behind it.
Why "roles," not model IDs
web/api/_models.js (web) and supabase/functions/_shared/models.js (Edge) are the single
sources of truth for model selection — the two files' MODEL_DEFAULTS block is kept
byte-identical between runtimes, and every call site picks a role, never a raw model ID.
Rolling a model to a new version is then a config change (set the matching environment variable),
not a code deploy or a grep-and-replace across call sites.
Model roles
| Role | Env override | Default | Used for |
|---|---|---|---|
board | SABLE_MODEL_BOARD | claude-opus-4-8 | The SABLE managed agent's morning/EOD board generation |
extract | SABLE_MODEL_EXTRACT | claude-sonnet-5 | Meeting extraction (Fathom, Granola, email) |
intake | SABLE_MODEL_INTAKE | claude-sonnet-5 | Kickoff intake synthesis |
synth | SABLE_MODEL_SYNTH | claude-sonnet-5 | Interview synthesis |
chat | SABLE_MODEL_CHAT | claude-haiku-4-5 | Ask SABLE scoped Q&A (/api/chat) |
chatStream | SABLE_MODEL_CHAT_STREAM | claude-sonnet-5 | Default streaming chat (/api/chat-stream) |
chatStreamPro | SABLE_MODEL_CHAT_STREAM_PRO | claude-opus-5 | Streaming chat for parity/board-upgrade users (SABLE_CHAT_OPUS_EMAILS) |
weekly | SABLE_MODEL_WEEKLY | claude-haiku-4-5 | Weekly-update draft generation |
record | SABLE_MODEL_RECORD | claude-sonnet-5 | Meeting super-summary (dense internal record) |
deliverable_authoring | SABLE_MODEL_DELIVERABLE_AUTHORING | claude-sonnet-5 | Client deliverable drafting |
vision | SABLE_MODEL_VISION, VISION_OCR_MODEL | claude-haiku-4-5 | Image/PDF OCR fallback for uploads |
embed | SABLE_MODEL_EMBED, VOYAGE_EMBED_MODEL | voyage-4 | Knowledge chunk embeddings |
rerank | SABLE_MODEL_RERANK, VOYAGE_RERANK_MODEL | rerank-2.5 | Retrieval reranking |
To roll a model: set the matching SABLE_MODEL_<ROLE> environment variable (for example
SABLE_MODEL_CHAT=claude-haiku-4-6), or change the default in _models.js/models.js. Also add a
pricing row in _pricing.js — a family fallback meters an unrecognized model in the meantime so
cost tracking never silently drops to null, but an explicit row is more accurate.
Anthropic: what each role actually calls
- Board — the SABLE Claude Managed Agent, invoked by Trigger.dev's report pipeline (see Reports and Boards Tasks), not a direct Messages API call from web code.
- Extract, intake, synth, record, deliverable_authoring — direct Anthropic Messages API calls
from Edge Functions (
fathom-sync,interview-synthesize) or web API routes (_deliverable_llm.js,_meeting_record.js), each passing its role throughMODELS.<role>(). - chat / chatStream / chatStreamPro —
/api/chatand/api/chat-stream.chatStreamupgrades a caller tochatStreamPro(Opus) only when their email is on theSABLE_CHAT_OPUS_EMAILSallowlist; everyone else gets the Sonnet default. - vision —
web/api/_vision.js. OCR for images and scanned PDFs with no extractable text layer, reusing the sameANTHROPIC_API_KEYas chat. Payloads are capped at 28 MB of base64 to stay under Anthropic's request size limit, and the function returns an empty string — never throws — whenever OCR is unavailable or yields nothing, so a document just stays text-only instead of failing the whole ingest.
Voyage AI: embeddings and reranking
web/api/_rag.js (and its Edge twin) chunk, embed, and index text into knowledge_chunks using
the embed role (voyage-4 by default) and rerank retrieval results using the rerank role
(rerank-2.5). Chunking is deterministic and testable: text is sanitized (control characters and
repeated whitespace stripped), hashed with SHA-256 for idempotent re-indexing, and long paragraphs
are split on a target-token budget with a configurable word-overlap tail so a chunk boundary
doesn't sever a sentence's context.
Exa: explicit public-web research
/api/research is the only route that reaches the open web. It is lead/admin-only, rate-limited
(SABLE_EXA_RATE_LIMIT, default 20/min) via the shared _rate_limit.js limiter, and results are
cached for SABLE_EXA_CACHE_TTL_HOURS (default 24, clamped 1–168). It requires EXA_API_KEY; when
unset the route has nothing to call. Private SABLE knowledge (the spine, documents, transcripts)
remains the default source for every other surface — Exa is opt-in and explicit, never a fallback
a user reaches by accident.
Cost tracking: ai_usage, frozen prices
Every AI call that goes through _usage.js's logUsage() writes one row to the ai_usage table:
provider, surface, model, operation, token counts, the acting person, project, and source, plus a
cost_usd computed at write time from _pricing.js. Logging is fire-and-forget and wrapped in
try/catch that never throws, modeled on the same pattern _rag.js uses for retrieval_events —
cost tracking must never be the reason a user-facing request fails.
Prices in _pricing.js are USD per 1,000,000 tokens, with an EFFECTIVE_PRICES history so a price
change only affects future rows — because token counts are stored, not just a computed cost,
a historical row can always be recomputed if a rate turns out to have been wrong. Anthropic prices
carry separate cache-write (1.25× base input, 5-minute TTL) and cache-read (0.10× base input)
tiers; Voyage embeddings and rerank are single-rate per token. The Admin Costs screen
(/admin-costs.html) reads this ledger.
Where the code lives
sable-agents-demo/web/api/_models.js,sable-agents-demo/supabase/functions/_shared/models.js.sable-agents-demo/web/api/_vision.js,_rag.js,_usage.js,_pricing.js.sable-agents-demo/web/api/research.js,_external_research.js.sable-agents-demo/web/api/chat-stream.js,chat.js.