Skip to documentation

Capability clusters

C4 · AI platform

Every LLM-backed capability in eli: a gateway-routed model layer with a fixed four-tier registry, a three-tier key resolver, per-call usage metering, seven native feature routes, a persisted RAG chat with function-calling tools over the vault, a daily digest pipeline, an OpenAI-compatible facade, AI template generation, and the full BYOK key lifecycle — with an opt-in HIPAA provider lockdown.

The AI platform is a Vercel-AI-Gateway-routed model layer with a fixed four-tier registry (fast/smart/cheap/embed pinned in lib/ai/models.ts), a three-tier key resolver (SaaS workspace BYOK → local BYOK → gateway master key, lib/ai/resolver.ts), per-call usage metering with read-time pricing, seven native /api/ai/* feature routes, a persisted multi-turn RAG chat with an AI-SDK function-calling toolset over the vault, a daily digest pipeline (cron + manual), an OpenAI-compatible /v1 facade for external tools, AI template generation, and the full BYOK key lifecycle including pgcrypto encryption at rest and an offline KMS rotation script. An opt-in HIPAA lockdown (ELI_HIPAA=1) constrains all gateway routing to a BAA-eligible provider allowlist.

Capabilities

CapabilityWhat it doesEntry pointsModes
Model tier registryFour tiers pinned in code (no env override): fast=openai/gpt-5.4-mini, smart=anthropic/claude-opus-4.6, cheap=google/gemini-3-flash, embed=openai/text-embedding-3-large; modelFor(tier) throws on unknown tier; EMBEDDING_DIMS=1536lib/ai/models.ts modelFor(), re-exported via lib/ai/index.tsboth
Gateway options builder + HIPAA lockdowngatewayOpts() builds {gateway:{order, only, sort, disallowPromptTraining:true}} for every AI SDK call; default failover anthropic→openai→google, sort cost; ELI_HIPAA=1 forces only to the BAA allowlist [anthropic, azure-openai, google-vertex] and throws on an explicit non-BAA orderlib/ai/gateway-opts.ts gatewayOpts(), GATEWAY_DEFAULTSboth
Persisted gateway routing configPer-caller {order[], sort} overrides: SaaS in workspaces.ai_gateway_config jsonb (RLS, owner/admin write), local in ~/.eli/config.json#aiGateway; drag-and-drop reorder UI over 9 known providers/ai-key, /w/[workspace]/settings/ai, app/_actions/ai-gateway.ts, components/ai/gateway-config-form.tsxboth
AI key resolverPriority: SaaS workspace BYOK (needs workspaceId+provider+ELI_KMS_KEY, service-role decrypt) → local BYOK (~/.eli/config.json#aiKey) → AI_GATEWAY_API_KEY master; returns {key, source} or null (routes 503 with setup hint)lib/ai/resolver.ts resolveAiKey()both
SaaS BYOK key managementPer-workspace per-provider keys, pgcrypto pgp_sym_encrypt with ELI_KMS_KEY, masked preview; upsert (=rotate), delete, list, test server actions/w/[workspace]/settings/ai, app/_actions/ai-keys.ts, lib/storage/supabase/ai-keys.ts, components/ai/keys-form.tsxsaas
Local BYOK key managementSingle plaintext key in ~/.eli/config.json#aiKey (mode 0600, OS-user trust); status/set/delete actions, two-state form/ai-key (404s outside local), app/_actions/ai-key-local.tslocal
BYOK key validation ("Test")Provider auth ping without the AI SDK: models-list GET for openai/xai/mistral, ?key= query for Google, 4-token claude-3-5-haiku-20241022 completion for Anthropic; 10s timeoutlib/ai/test-key.ts, testAiKeyAction (admin/owner-gated)saas
KMS key rotation CLIRe-encrypts every ai_keys row from ELI_KMS_KEY_OLD to ELI_KMS_KEY (decrypt RPC → upsert RPC → verify round-trip); refuses keys < 16 chars or identical; exit 3 on any row failurenode scripts/rotate-ai-keys.mjssaas
Usage metering + pricingrecordUsage() writes one row per call (route, model, tokens, key source) into Postgres ai_usage (SaaS) or the vault SQLite ai_usage (local); best-effort; cost computed at read time from an in-code price table (unknown models = $0); daily / by-route / by-model rollups rendered by usage cardslib/ai/usage.ts, lib/ai/pricing.ts, components/ai/usage-card.tsx, components/ai/usage-breakdown.tsxboth
POST /api/ai/draftStreamed short-form completion (fast tier default, tierselectable); powers the BlockNote slash-menu "AI continue" (last 8k chars) and "AI generate from prompt"app/api/ai/draft/route.tsboth
POST /api/ai/summarizeNote TL;DR (fast tier, 20–500 words, 30k-char input cap); optional persistToFrontmatter writes a summary key via read-modify-writeapp/api/ai/summarize/route.tsboth
POST /api/ai/transformInline rewrite/shorten/expand/translate/grammar with per-action system prompts; source wrapped in --- delimiters as prompt-injection mitigationapp/api/ai/transform/route.ts, components/editor/ai-transform-menu.tsxboth
POST /api/ai/researchMulti-note map-reduce Q&A over 1–20 notes: fast-tier chunk summaries (≤50 chunks) then smart-tier synthesis citing note titles; API/agent-only, no UIapp/api/ai/research/route.tsboth
POST /api/ai/extract-tasksSmart tier emits strict-JSON tasks (defensive parser, cap 50); creates one child note per task under tasks/<source-slug>/ with type:task frontmatter; API/agent-onlyapp/api/ai/extract-tasks/route.tsboth
POST /api/ai/continueContinue-writing at a cursor (smart tier, streamed); trailing 4KB as immediate context; no current UI consumer (editor uses /api/ai/draft)app/api/ai/continue/route.tsboth
POST /api/ai/chatMulti-turn RAG chat (non-streaming): thread history prepend, hybrid search on the last user turn, fast-tier map over ≤24 chunks, smart-tier reduce, optional vault tools with a 6-step cap, owner-gated persistenceapp/api/ai/chat/route.tsboth
POST /api/ai/chat/streamToken-streaming variant of the same pipeline; persists via onFinish; separate route because the content type differs; no first-party UI consumer (VaultChat calls the non-streaming route)app/api/ai/chat/stream/route.tsboth
GET /api/ai/chat/messagesThread history fetch; SaaS non-owners get 404 (not an empty array)app/api/ai/chat/messages/route.tsboth
Chat thread persistence + CRUDMode-dispatched list/create/get/append/rename/delete; SaaS chat_threads/chat_messages RLS-gated to owner; local same tables in the per-vault SQLite indexlib/chat/storage.ts, app/_actions/chat.tsboth
VaultChat UIThread sidebar + conversation pane with cited source links and model/key-source footer; "tools" and "allow writes" checkboxes (writes disabled unless tools on); Cmd/Ctrl+Enter send/chat (local), /w/[workspace]/chat (saas), components/ai/vault-chat.tsxboth
AI vault tool registrybuildVaultTools(): read_note, list_notes, search_notes always; create_note/update_note only when allowWrites=true; agent writes stamp authorKind:'agent', agentName:'chat-tool' version rows; used by chat routes and /v1/chat/completions with stepCountIs(6)lib/ai/tools.ts buildVaultTools()both
AI write-tool audit loggingrecordAiAudit() inserts audit_log rows (ai.tool.create_note/ai.tool.update_note) via service role on SaaS agent writes, best-effort; local relies on authorKind:'agent' version rows insteadlib/ai/audit.tssaas
Daily digest systemPure buildDigestMarkdown(created/updated split, top-8 tags, optional smart-tier "Highlights" that degrades silently on LLM failure) + runDigest idempotent upsert of digests/YYYY-MM-DD.md; cron fans out over ALL workspaces via service role, manual action is session-scopedGET|POST /api/cron/digest (13:00 UTC), generateDigestNowAction, /v/digest, /w/[workspace]/digest, components/digest/digest-now-button.tsxboth
POST /v1/chat/completionsOpenAI-compatible facade for external clients: PAT bearer (SaaS) or ELI_LOCAL_TOKEN (local); model maps to tiers (eli/<tier> pins, mini/haiku/flash → fast, else smart); RAG injects first 4k chars of each hybrid hit; eli_use_tools/eli_allow_writes/eli_top_k extensions; stream:true re-encodes into OpenAI SSE ending data: [DONE]app/v1/chat/completions/route.tsboth
POST /v1/embeddingsSame auth; delegates to embedTexts() (1536-dim); ≤256 inputs per request, dimensions must equal 1536 if given; OpenAI list shape; usage tokens approximated at chars/4app/v1/embeddings/route.tsboth
GET /v1/modelsLists eli/<tier> aliases plus gateway-resolved provider ids, OpenAI catalog format, no authapp/v1/models/route.tsboth
GET /v1/openapi.jsonHand-written OpenAPI 3.1 spec covering /v1/* plus the eli-native AI/search/export/import/cron surface; documents the eli_pat_<prefix>_<secret> PAT format; cached 300sapp/v1/openapi.json/route.tsboth
AI template generationSmart tier writes a template skeleton with {{date:...}}/{{var:...}}/{{prompt:...}} placeholders; persists to <vault>/.eli/templates/<type>.md (local) or a _template:true note (saas); records usagegenerateTemplateAction (app/_actions/templates-ai.ts)both
AI settings pagesAssemble key form (mode-specific), EmbedIndexerCard, gateway config form, usage cards; SaaS page tolerates RLS-denied key list for non-admins/ai-key, /w/[workspace]/settings/aiboth
Rate limiting on all AI routesEvery /api/ai/* and /v1/* POST calls checkRateLimit (60/min, 10,000/day) keyed user:<id> / pat:<id> / shared 'local' sentinel; 429 with retry-after; in-memory per-instance store only (ELI_RATELIMIT_BACKEND=redis throws — adapter unshipped)lib/mcp/ratelimit.ts checkRateLimit()both

System view — simplified

System view — simplified

The AI platform is a thin, uniform stack: every caller — a UI component, an external OpenAI SDK client, or a cron job — hits a route that validates, authenticates, rate-limits, resolves a secret, and calls the Vercel AI Gateway with a tier-pinned model id and routing options. Standalone, this cluster is a complete "AI workbench over a markdown vault": with only a vault (or workspace) and one API key you get streamed drafting, summarize/transform, task extraction, map-reduce research, a persisted RAG chat with function-calling tools that can read and (opt-in) write notes, a daily digest note, and an OpenAI-compatible endpoint any third-party chat client can point at. All model traffic goes through the gateway — eli never talks to a provider API directly (the sole exception is the BYOK "Test" ping in lib/ai/test-key.ts), which is what makes failover, cost-sort routing, and the HIPAA provider lockdown single-point enforceable.

System view — detailed

System view — detailed
ComponentRoleFiles
Model registry + gateway optionsStatic tier→model pins, embedding dims, gateway routing options builder with HIPAA gate, isGatewayConfigured()lib/ai/models.ts, lib/ai/gateway-opts.ts, lib/ai/index.ts
Persisted gateway config loaderReads per-workspace jsonb (SaaS, RLS-scoped) or ~/.eli/config.json#aiGateway (local), narrows to {order, sort}lib/ai/gateway-config.ts, app/_actions/ai-gateway.ts, components/ai/gateway-config-form.tsx, lib/config/index.ts, lib/config/schema.ts
Key resolverThree-tier secret selection with source tag for telemetrylib/ai/resolver.ts
BYOK key management (SaaS)pgcrypto-encrypted per-workspace provider keys: CRUD actions, storage RPC helpers, test-key pings, admin UIapp/_actions/ai-keys.ts, lib/storage/supabase/ai-keys.ts, lib/ai/test-key.ts, components/ai/keys-form.tsx, app/(saas)/(app)/w/[workspace]/settings/ai/page.tsx
BYOK key management (Local)Single plaintext key in ~/.eli/config.json (0600) with status/set/delete actions and two-state formapp/_actions/ai-key-local.ts, components/ai/local-key-form.tsx, app/(local)/ai-key/page.tsx
KMS rotation scriptOffline re-encryption of all ai_keys rows old→new KMS key with per-row verifyscripts/rotate-ai-keys.mjs
Usage metering + pricingPer-call token rows (SQLite/Postgres), in-code USD price table, daily/by-route/by-model rollups, display cardslib/ai/usage.ts, lib/ai/pricing.ts, components/ai/usage-card.tsx, components/ai/usage-breakdown.tsx
Vault tool registry + auditFunction-calling tools (read/list/search + gated create/update) closed over workspace context; SaaS audit_log rows on writeslib/ai/tools.ts, lib/ai/audit.ts
AI feature routesSix native endpoints sharing the auth → rate-limit → key-resolution guard stackapp/api/ai/{draft,summarize,transform,research,extract-tasks,continue}/route.ts
Chat systemMulti-turn RAG chat, streaming variant, history fetch, mode-dispatched persistence, thread CRUD actions, VaultChat UI, both pagesapp/api/ai/chat/{route,stream/route,messages/route}.ts, lib/chat/{storage,types}.ts, app/_actions/chat.ts, components/ai/vault-chat.tsx, app/(local)/chat/page.tsx, app/(saas)/(app)/w/[workspace]/chat/page.tsx
Digest systemPure markdown builder + shared runner (idempotent upsert, optional smart-tier highlights), cron fan-out route, session-scoped manual action, buttonlib/ai/digest.ts, lib/ai/digest-runner.ts, app/api/cron/digest/route.ts, app/_actions/digest.ts, components/digest/digest-now-button.tsx, vercel.json
v1 OpenAI-compatible facadePAT/local-token chat completions (RAG + eli_ extensions + SSE re-encoding), embeddings, model list, OpenAPI specapp/v1/{chat/completions,embeddings,models,openapi.json}/route.ts
AI template generatorSmart-tier template skeletons with placeholder syntax, persisted to .eli/templates/app/_actions/templates-ai.ts

Data flow — simplified

Data flow — simplified

The load-bearing flow is the multi-turn ask-the-vault chat: the client sends only the new user turn plus a threadId; the server prepends stored history, retrieves context by hybrid search over the last user turn, compresses it via a fast-tier map step, and answers with a smart-tier reduce that can optionally call vault tools. Persistence is strictly owner-gated and never sinks the response on failure.

Data flow — detailed

Three sequences carry this cluster at runtime: the RAG chat with the tool-call loop, the /v1 facade serving an external OpenAI client, and the digest cron fan-out.

(a) Multi-turn RAG chat with tool loop (app/api/ai/chat/route.ts; the /chat/stream variant differs only in returning a UI-message stream and persisting in onFinish):

Data flow — detailed (a): multi-turn RAG chat with the tool-call loop

(b) /v1/chat/completions from an external OpenAI client (app/v1/chat/completions/route.ts):

Data flow — detailed (b): the /v1 facade serving an external OpenAI client

(c) Digest cron run (app/api/cron/digest/route.ts lib/ai/digest-runner.tslib/ai/digest.ts):

Data flow — detailed (c): the digest cron fan-out

The manual path (generateDigestNowAction, app/_actions/digest.ts) reuses the same runDigest pipeline but is strictly session-scoped: it resolves the workspace from the eli_ws cookie plus membership, never uses the service role, never fans out, and tags generated_by: eli-manual. Digest browsing pages (/v/digest, /w/[workspace]/digest) discover digests by frontmatter type:digest, not path — see 08-capture-templates siblings and the trash/digest surfaces in 01-core-data-storage.

Works separately / works together

Standalone

With every other cluster reduced to its minimal interface (a storage adapter and a search function), the AI platform is a self-contained product: point one API key at it (AI_GATEWAY_API_KEY, a local BYOK key, or a workspace BYOK key) and you get drafting, summarizing, transforming, task extraction, research Q&A, a persisted chat that can read and write your notes with an audit trail, a daily digest note, and — via /v1 — the ability to use any off-the-shelf OpenAI client (Open WebUI, librechat, plain SDK scripts) as a frontend for your vault. Key lifecycle (encrypt, test, rotate, KMS re-encryption) and cost visibility (usage rows + read-time pricing) are built in, so the cluster is operable without any external billing or observability system. The HIPAA lockdown is a single env flag that hard-constrains every model call in the process.

Composed

  • 03-semantic-search → chat/research/v1: hybridSearch(storage, query, {limit, workspaceId?}) drives RAG discovery in app/api/ai/chat/route.ts:178, chat/stream, research, and app/v1/chat/completions/route.ts:179; the search_notes tool wraps the same function (lib/ai/tools.ts:132-156); chunkText (3500/250) feeds the map steps; /v1/embeddings delegates to embedTexts(); MODELS.embed + EMBEDDING_DIMS=1536 are the canonical pins the embedding pipeline consumes.
  • 01-core-data-storage ← tools/digest/extract-tasks: all reads/writes go through getStorage(); the digest cron constructs LocalAdapter.forVault / SupabaseAdapter.fromClient directly (app/api/cron/digest/route.ts:87,116); extract-tasks creates child notes under tasks/<source-slug>/.
  • 06-versioning-history ← agent writes: update_note writes with {authorKind:'agent', agentName:'chat-tool'} (lib/ai/tools.ts:216), so every AI edit is a distinguishable version row.
  • 07-saas-collaboration ↔ keys/audit/config: ai_keys (migration 0011), workspaces.ai_gateway_config (0012), and chat_threads/chat_messages (0014) live in the shared schema; recordAiAudit() lands ai.tool.* rows in the same audit_log the /settings/audit UI reads.
  • 05-agent-surface ↔ /v1: /v1 routes reuse lib/auth/pat (extractBearer/parseBearer/verifyHash) against personal_access_tokens; rate limiting reuses lib/mcp/ratelimit; /v1/openapi.json advertises the whole AI surface to external agents; the same PAT credential covers /v1 chat + embeddings.
  • 02-authoring-ux → feature routes: BlockNote slash-menu "AI continue"/"AI generate" call /api/ai/draft (components/editor/blocknote-editor.tsx:165-230), the toolbar TL;DR calls /api/ai/summarize, and AiTransformMenu calls /api/ai/transform.
  • 08-capture-templates ← template generation: generateTemplateAction writes .eli/templates/<type>.md consumed by lib/templates/apply.ts and the template editor.
  • 09-platform-ops → cron/env: vercel.json schedules /api/cron/digest at 0 13 * * *; CRON_SECRET, ELI_KMS_KEY, ELI_HIPAA, AI_GATEWAY_API_KEY are the operative env knobs; scripts/rotate-ai-keys.mjs is an ops runbook item.

Load-bearing details

  • Model pins are code, not env: lib/ai/models.ts:31-40 deliberately rejects per-tier env overrides; the gateway routing config is the sanctioned override knob. EMBEDDING_DIMS=1536 at models.ts:62.
  • HIPAA lockdown: ELI_HIPAA=1 forces only to [anthropic, azure-openai, google-vertex] (lib/ai/gateway-opts.ts:39,110) and throws on any explicit caller order naming a non-BAA provider (gateway-opts.ts:89-103); disallowPromptTraining: true on every call (gateway-opts.ts:112).
  • Key resolution order is saas-byok → local-byok → gateway-default (lib/ai/resolver.ts:35-74). SaaS BYOK is only consulted when the caller passes BOTH workspaceId and provider — so routes without a provider body param (app/v1/chat/completions/route.ts:194-196, lib/ai/digest-runner.ts) can never use per-provider SaaS BYOK and silently fall through to the gateway master key.
  • The resolved key rides as a per-call Authorization: Bearer header on the gateway request (app/api/ai/chat/route.ts:202); the source tag rides through responses and usage rows for telemetry.
  • Chat map-reduce numbers: top-K default 8 (max 20), ≤24 chunks of 3500 chars / 250 overlap (chat/route.ts:213-222), fast-tier ≤100-word summaries, smart-tier reduce with cite-by-title instructions, tool loop capped by stepCountIs(6) (chat/route.ts:272,297).
  • Chat persistence contract: only the turns received in the current call plus the final assistant reply are appended, and only when the thread exists AND is owned by the caller (local skips the owner check) (chat/route.ts:157-166,320-330); intermediate tool turns are never persisted; persistence failure only warns.
  • Write tools are double-gated: useTools must be true AND allowWrites must be true for create_note/update_note to even appear in the toolset (lib/ai/tools.ts:164); the VaultChat UI mirrors this (allow-writes checkbox disabled unless tools checked, components/ai/vault-chat.tsx:283-305).
  • SaaS agent writes produce best-effort audit_log rows ai.tool.create_note/ai.tool.update_note via service role (lib/ai/tools.ts:187,219, lib/ai/audit.ts); local mode's trail is the authorKind:'agent' version rows.
  • /v1 local-mode auth quirk: when ELI_LOCAL_TOKEN is unset, ANY non-empty bearer is accepted (app/v1/chat/completions/route.ts:91-99, same in embeddings); SaaS requires an eli_pat_<prefix>_<secret> PAT with hash verification, revocation, and expiry checks (route.ts:102-122).
  • /v1/chat/completions injects raw note bodies (first 4,000 chars per hit, route.ts:183-190) as RAG context, unlike /api/ai/chat which injects map-step summaries; tierFromModel maps eli/<tier> pins, ids containing mini/haiku/flash → fast, everything else → smart (route.ts:66-78).
  • Pricing is an in-code table (lib/ai/pricing.ts:27-36: e.g. anthropic/claude-opus-4.6 at $15/$75 per MTok) with a $0 fallback for unknown models (pricing.ts:38-41) — pricing changes reprice history at read time without a backfill.
  • Rate limits: 60/min and 10,000/day defaults (lib/mcp/ratelimit.ts:37-38), keyed user:<id> / pat:<id> / shared 'local' sentinel in local mode.
  • Digest cron: vercel.json schedules 0 13 * * *; badAuth() refuses ALL requests when CRON_SECRET is unset (app/api/cron/digest/route.ts:46-51); SaaS fans out over ALL workspaces via SupabaseAdapter.fromClient on the service-role client (route.ts:106-132), while generateDigestNowAction is strictly session-scoped to the eli_ws cookie workspace and never service-role (app/_actions/digest.ts).
  • Digest delivery is in-app only — a digests/YYYY-MM-DD.md note with type:digest frontmatter; no email/Resend integration exists; LLM Highlights failures degrade silently to the deterministic body (lib/ai/digest.ts).
  • SaaS BYOK crypto: pgp_sym_encrypt via ai_keys_upsert RPC keyed by ELI_KMS_KEY (min 16 chars); plaintext never returned from any Server Action; the rotation script verifies round-trip decrypt per row and exits 3 on any failure (scripts/rotate-ai-keys.mjs).
  • Anthropic key test is a 4-token claude-3-5-haiku-20241022 completion because Anthropic has no unauthenticated models-list endpoint (lib/ai/test-key.ts:51-54,109-140); Google receives the key as a ?key= query param.
  • summarize persistToFrontmatter does a read-modify-write merge because updateNote replaces the frontmatter map wholesale (app/api/ai/summarize/route.ts:145-156).
  • Local-mode chat threads/messages and ai_usage rows live in the per-vault SQLite index db — switching the active vault switches conversation history and usage stats (lib/chat/storage.ts:186-198, lib/ai/usage.ts:71-85).
  • extract-tasks hard limits: source truncated at 50k chars, max 50 tasks, titles clipped to 200 chars, due dates must match YYYY-MM-DD (app/api/ai/extract-tasks/route.ts:58-88).
  • Other input caps: draft prompt ≤20k chars, transform text ≤20k + hint ≤500, research maxChunks ≤50 (default 30) with ≤120-word map summaries, chat messages[] ≤50 — and the chat routes still accept the legacy single-shot {question} body (3–2,000 chars, synthesized into one user turn, app/api/ai/chat/route.ts:75,142).
  • Vault tool caps: read_note truncates bodies at 30k chars with a [truncated] marker, list_notes limit ≤100 (default 20), search_notes limit ≤50 (default 10) (lib/ai/tools.ts:90,99,129).
  • SaaS recordUsage silently drops rows without a workspaceId (lib/ai/usage.ts:55) — a metered call lacking workspace context produces no usage row at all.
  • Chat storage encodings: thread lists are capped at 50 ordered by updated_at desc in both modes (lib/chat/storage.ts:52-53,204); local chat_messages store sources/tool_calls as JSON strings, and appends manually bump chat_threads.updated_at because SQLite has no touch trigger (storage.ts:288-295).
  • SaaS renameChatThreadAction/deleteChatThreadAction perform no app-level ownership check — RLS (migration 0014 policies) is the only gate (app/_actions/chat.ts:97-127).
  • Digest bullet formats diverge by mode: SaaS emits - [title](/w/<slug>/n/<path>) markdown links, local emits [[path|title]] wikilinks (lib/ai/digest.ts:72-82).
  • SaaS BYOK accepts exactly five providers (openai/anthropic/google/xai/mistral) with plaintext 8–500 chars (app/_actions/ai-keys.ts:37-38,111).
  • The /v1 SSE stream iterates result.textStream only — tool-call activity never appears in the emitted OpenAI chunk frames, just text deltas (app/v1/chat/completions/route.ts:287-297).

Gaps and partially wired

  • Usage metering coverage is partial (verified by grep of recordUsage call sites): only /api/ai/draft, /continue, /summarize, /extract-tasks, /transform, the digest runner's LLM hook, and generateTemplateAction record usage. /api/ai/chat, /api/ai/chat/stream, /api/ai/research, /v1/chat/completions, and /v1/embeddings record nothing — the most expensive surfaces (smart-tier chat) are invisible in the usage cards.
  • /api/ai/continue and /api/ai/extract-tasks have no UI consumer — the editor's "AI continue" slash command uses /api/ai/draft with the last 8k chars instead (components/editor/blocknote-editor.tsx:165-196). Both are API/agent-only, advertised via /v1/openapi.json.
  • /api/ai/research has no UI consumer — API/agent-facing only, and it does not record usage.
  • renameChatThread exists in lib/chat/storage.ts and as a server action (app/_actions/chat.ts) but has no UI consumer in VaultChat — threads keep their first-60-chars title forever.
  • /api/ai/chat/stream has no first-party UI consumer — VaultChat calls the non-streaming /api/ai/chatand shows a "Thinking…" placeholder (components/ai/vault-chat.tsx:155); the streaming variant is API/agent-only today.
  • Rate limiting is per-instance memory only ELI_RATELIMIT_BACKEND=redis is env-selectable but throws at startup because lib/mcp/ratelimit-redis.ts never shipped (lib/mcp/ratelimit.ts:183-192); multi-instance SaaS deployments multiply the effective limits.
  • /v1 local-token fail-open: with ELI_LOCAL_TOKEN unset, any non-empty bearer authenticates the local /v1 surface — a deliberate local-trust choice, but a footgun if a local instance is port-forwarded.
  • SaaS BYOK unreachable from provider-less routes: /v1/chat/completions and the digest runner call resolveAiKey without a provider, so workspace BYOK keys are never used there; those calls always bill the gateway master key.
  • Unknown model ids price at $0 in the usage UI rather than flagging — stale price-table entries silently understate costs (lib/ai/pricing.tsheader comment: "Rates as of 2026-Q2").
  • No email delivery for digests— in-app note only; the digest pages' empty states point at a manual curl backfill against /api/cron/digest with CRON_SECRET.
  • Local /v/digest has no inbound nav or command-palette link — URL-only surface (SaaS has a top-nav Digest link).
  • Intermediate tool turns are never persisted to chat threads — replaying a thread loses the tool-call evidence; the only durable trail is version rows + SaaS audit_log.
  • SaaS digest/chat pages resolve workspace chrome from the URL slug but notes from the eli_ws cookie workspace (shared platform quirk, see 07-saas-collaboration) — divergence shows one workspace's chrome with another's data.