# AI foundation

How every AI feature gets its model from one registry on TanStack AI, its flag, the plan quota it counts against, and the scrubber on everything it sends.

## What it does [#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](/docs/adr/0017-ai-model-registry-and-quotas); the product rules are in `docs/specs/ai-features.md`.

## Configure a model [#configure-a-model]

| Variable                                                  | Value                                                                                                                   |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `AI_PROVIDER`, `AI_MODEL`                                 | the text model every text feature uses unless it has its own ([ADR-0007](/docs/adr/0007-ai-assistant-and-tool-catalog)) |
| `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 [#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 [#adding-an-ai-feature]

1. A row in `AI_FEATURES` (kind, flag, quota) and, if new, its flag in `packages/flags/src/registry.ts`.
2. In the service: `admitAi(db, systemDb, userId, plan, feature, platform)` returns the model or fails with `AiUnavailable`, `AiRateLimited` or `AiQuotaExceeded`; then `generateStructured`, `generateImages`, `transcribe` or `embedTexts` with it. Map failures with `aiRpcError`.
3. Tests use `fakeText` from `@tanstack/ai/testing` and `fakeImage`, `fakeTranscription`, `fakeEmbedding` from `packages/api/src/testing/ai-adapters.ts`; no test calls a live model.

## Data and privacy [#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](/docs/adr/0009-row-level-security)). 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 [#procedures]

| Procedure  | Who                | Returns                                                         |
| ---------- | ------------------ | --------------------------------------------------------------- |
| `ai.usage` | any signed-in User | the plan and this month's use of each quota whose feature is on |

