Skip to main content
Localizia

AI

Overview

The kit ships a base structure for AI, not an AI feature: a provider abstraction in src/lib/ai/ (same pattern as email and payments) that resolves the configured model, plus one reference procedure (ai.generate) showing how to meter a model call with credits end-to-end. What you build with the model — chat, extraction, agents, streaming — is your product's decision, along with every parameter you pass.

Everything is off until you enable it:

AI_ENABLED=true
AI_PROVIDER=anthropic        # anthropic | openai | google
ANTHROPIC_API_KEY=sk-ant-...
# AI_MODEL=claude-opus-4-8   # optional override; defaults per provider

If the selected provider's key is missing, the app fails at boot (Zod validation in src/env.ts). Default models live in src/config/ai.config.ts (claude-sonnet-4-5 / gpt-4o-mini / gemini-2.5-flash).

Using the model

Two entry points, both in src/lib/ai/ — AI SDKs are imported only there (the same rule as Stripe and Resend):

generateAiText(opts) — non-streaming convenience with normalized usage:

import { generateAiText } from "@/lib/ai";

const { text, usage } = await generateAiText({
  prompt: "Summarize this…",
  system: "You are terse.",       // optional
  maxOutputTokens: 512,           // optional — you decide the parameters
});
// usage: { inputTokens, outputTokens, totalTokens } — always numbers

resolveLanguageModel() — the escape hatch for everything else. It returns the configured AI SDK LanguageModel, so any SDK capability works: streaming, tools, structured output…

import { streamText } from "ai";               // allowed inside src/lib/ai only —
import { resolveLanguageModel } from "@/lib/ai"; // build your feature module there

const result = streamText({ model: await resolveLanguageModel(), prompt });

Metering with credits — the reference wiring

ai.generate (tRPC, any org member) is the blueprint for charging model usage through the credits lifecycle:

  1. Estimate before any work: ~chars/4 input tokens + the output cap, priced via estimateCredits("ai.chat", tokens/1000) (cost catalog in credits.config.ts).
  2. Reserve → generate → settle with withCreditHold: the hold is taken atomically (throws errors.insufficientCredits before calling the model if the balance can't cover it), the model runs, and the hold settles with the actual cost computed from the provider-reported usage.totalTokens — unused estimate refunded, failures release the hold.
  3. The response includes creditsCharged so the client can show real cost.

Copy this shape for your own AI features: keep the model call inside your procedure, estimate generously, settle with real usage.

Testing without network

ai/test ships MockLanguageModelV4 — inject it through generateAiText's model option to test AI-consuming code with canned responses and exact token usage (see src/lib/ai/index.test.ts for the pattern).

Extending

  • Change models per call: pass your own model to generateAiText, or use resolveLanguageModel() and the SDK directly.
  • New provider: add a module under src/lib/ai/providers/, extend the AI_PROVIDER enum in env.ts and the defaults in ai.config.ts.
  • Pricing: tune the "ai.chat" entry (or add operations) in the creditCosts catalog.

The chat module (AI_CHAT_ENABLED)

A reference streaming, tool-calling, credit-metered chat at /chat, off by default. Enable with AI_CHAT_ENABLED=true (requires AI_ENABLED); behaviour (step caps, output caps, tool approval policy, rate limit) lives in src/config/ai-chat.config.ts.

What it solves once so products don't improvise it:

  • Streaming metering. Each turn RESERVES credits before any token streams (insufficient balance → refused before spend), SETTLES with the provider-reported usage when the stream finishes, and RELEASES the hold on error or abort. The estimate scales by the step cap because a tool loop re-sends the prompt per step — generous on purpose, the remainder refunds.
  • Tool safety. Tools are approval-required by default: the model's request to run one pauses the stream and renders an allow/deny prompt in the UI (signed approval state — a client can't forge one). Auto-approve read-only tools via autoApprovedTools. Every EXECUTED call lands in the audit log with hashed args.
  • Tools come from the product seam (src/product/ai-chat.ts): in-process AI SDK tool()s, and/or MCP servers — the kit connects provider-agnostically (@modelcontextprotocol/sdk + dynamicTool), so any provider can call MCP tools. MCP auth tokens are resolved server-side; user-configured MCP URLs are re-checked against the shared SSRF guard (src/lib/net/ssrf.ts) on every connect.

Conversations persist per (org, author) as structured UIMessage parts (AiConversation/AiMessage), so tool interactions replay faithfully when a conversation reloads.