What it does
Every AI feature (the assistant, receipt and voice structuring, the Studio, insights, transcription, embeddings) resolves its model from one registry, packages/api/src/features/ai/ai.models.ts, and calls it through TanStack AI. Each feature has a flag, off by default, and the calls a User makes are counted against the plan's monthly quota. The decision record is ADR-0017; the product rules are in docs/specs/ai-features.md.
Configure a model
| Variable | Value |
|---|---|
AI_PROVIDER, AI_MODEL | the text model every text feature uses unless it has its own (ADR-0007) |
AI_MODELS | JSON, feature to provider:model, e.g. {"studio":"gemini:gemini-3-pro-image-preview"} |
ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY | the key of each provider in use |
Image, transcription and embedding features have no default model. A provider without an adapter for the kind (Anthropic for images) or malformed JSON fails loudly on first use. Then turn the feature's flag on at /admin/flags: ai.assistant, ai.capture, ai.studio, ai.genui, ai.insights, ai.extras.
Quotas
| Quota | Free | Plus |
|---|---|---|
structuring: scanner, voice, listing and translation by a model | 20 | 300 |
studio: Studio images | 3 | 60 |
copilot: assistant questions | 20 | 500 |
Months are calendar months in UTC. A call counts before the model runs, under a per-User lock, so the limit is exact. Past it the procedure answers QUOTA (HTTP 402). The on-device scanner never counts.
Adding an AI feature
- A row in
AI_FEATURES(kind, flag, quota) and, if new, its flag inpackages/flags/src/registry.ts. - In the service:
admitAi(db, systemDb, userId, plan, feature, platform)returns the model or fails withAiUnavailable,AiRateLimitedorAiQuotaExceeded; thengenerateStructured,generateImages,transcribeorembedTextswith it. Map failures withaiRpcError. - Tests use
fakeTextfrom@tanstack/ai/testingandfakeImage,fakeTranscription,fakeEmbeddingfrompackages/api/src/testing/ai-adapters.ts; no test calls a live model.
Data and privacy
ai_usage keeps one row per counted call: the User, the quota and the time, never a prompt, an answer or an amount. It belongs to the User across households, tenant traffic only reads and adds its own rows, and deleting the User deletes them (ADR-0009). Every chat call ends with the scrubOutbound middleware, which removes plate, VIN and email shapes from the provider's copy of the conversation; image prompts and embedding inputs are scrubbed too.
Procedures
| Procedure | Who | Returns |
|---|---|---|
ai.usage | any signed-in User | the plan and this month's use of each quota whose feature is on |