Skip to main content

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

RoleEnv overrideDefaultUsed for
boardSABLE_MODEL_BOARDclaude-opus-4-8The SABLE managed agent's morning/EOD board generation
extractSABLE_MODEL_EXTRACTclaude-sonnet-5Meeting extraction (Fathom, Granola, email)
intakeSABLE_MODEL_INTAKEclaude-sonnet-5Kickoff intake synthesis
synthSABLE_MODEL_SYNTHclaude-sonnet-5Interview synthesis
chatSABLE_MODEL_CHATclaude-haiku-4-5Ask SABLE scoped Q&A (/api/chat)
chatStreamSABLE_MODEL_CHAT_STREAMclaude-sonnet-5Default streaming chat (/api/chat-stream)
chatStreamProSABLE_MODEL_CHAT_STREAM_PROclaude-opus-5Streaming chat for parity/board-upgrade users (SABLE_CHAT_OPUS_EMAILS)
weeklySABLE_MODEL_WEEKLYclaude-haiku-4-5Weekly-update draft generation
recordSABLE_MODEL_RECORDclaude-sonnet-5Meeting super-summary (dense internal record)
deliverable_authoringSABLE_MODEL_DELIVERABLE_AUTHORINGclaude-sonnet-5Client deliverable drafting
visionSABLE_MODEL_VISION, VISION_OCR_MODELclaude-haiku-4-5Image/PDF OCR fallback for uploads
embedSABLE_MODEL_EMBED, VOYAGE_EMBED_MODELvoyage-4Knowledge chunk embeddings
rerankSABLE_MODEL_RERANK, VOYAGE_RERANK_MODELrerank-2.5Retrieval 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 through MODELS.<role>().
  • chat / chatStream / chatStreamPro/api/chat and /api/chat-stream. chatStream upgrades a caller to chatStreamPro (Opus) only when their email is on the SABLE_CHAT_OPUS_EMAILS allowlist; everyone else gets the Sonnet default.
  • visionweb/api/_vision.js. OCR for images and scanned PDFs with no extractable text layer, reusing the same ANTHROPIC_API_KEY as 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.