AI-Ready Module
AI-Ready Module
What it does
A thin, reusable wrapper around the Anthropic SDK (lib/ai.ts) plus one
worked example built on top of it: an "Improve with AI" button on the
contact form that rewrites a
visitor's rough draft into a clearer message. Every call is logged to an
ai_usage table with token counts and an estimated USD cost, and the
feature is gated behind both a configured API key and a feature flag —
see feature-flags.md.
How to configure it
| Var | Purpose |
|---|---|
ANTHROPIC_API_KEY | Unset → isAiConfigured is false and every AI feature no-ops (returns a friendly "not available" error, never crashes). |
ANTHROPIC_MODEL | Optional override; defaults to claude-haiku-4-5-20251001 in lib/ai.ts (not in lib/env.ts, so a missing var reads as "use the default" rather than as an actual empty string in env output). |
Also requires the ai_assistant feature flag enabled (off by default) —
toggle it at /admin/feature-flags, see feature-flags.md.
How it works
lib/ai.ts— feature-agnostic on purpose: no DB dependency of its own.generateText({system, prompt, maxTokens})calls the Anthropic Messages API and returns{text, model, inputTokens, outputTokens}; callers write their ownai_usagerow tagged with their ownfeaturename, so this file never grows call-site-specific branches as more AI features get added on top of it.getClient()lazily constructs theAnthropicclient (not at module load) so importing this file never throws just because the key isn't set — same lazy-init pattern aslib/stripe.ts.estimateCostUsd(result)— multiplies token counts by a hardcodedPRICING_PER_MILLION_TOKENStable (illustrative published rates, not your actual negotiated pricing) keyed by model name; an unrecognized model (e.g. you setANTHROPIC_MODELto something not in the table) still gets its token counts logged, just withcostUsd: null.
lib/db/schema/app.ts'saiUsagetable — one row per successful call:feature(a free-form string tag, e.g."contact.improve_message"),model,inputTokens/outputTokens, nullablecostUsd, nullableuserId(onDelete: "set null"— usage history outlives the user being deleted).lib/actions/ai.ts'simproveContactMessage(raw)— the one shipped example. ChecksisAiConfiguredand theai_assistantflag server-side before doing anything else — a request reaching this action with either gate off is either a stale client render or a direct call, and either way shouldn't spend an API call. Then: Zod-validates length (10–4000 chars), rate-limits by IP (lib/rate-limit.ts, 10/hour — tighter thanlib/actions/contact.ts's own limit, deliberately, since this one costs real money per call), best-effort-attributes to a signed-in session if one exists (the contact page itself requires no login, souserIdis oftennull), callsgenerateText, and writes theai_usagerow before returning the rewritten text.components/marketing/contact-form.tsx— callsimproveContactMessagedirectly as an async function (not a<form action>, since it fills a textarea rather than submitting), same pattern aslib/actions/waitlist.ts'smarkInviteUsed.
How to extend it
- A new AI feature — call
generateText()from a new Server Action, write your ownai_usagerow with a distinctivefeaturetag, and gate it behindisAiConfigured(and optionally a new feature flag if you want it independently toggleable). No changes needed tolib/ai.tsitself. - Streaming responses —
generateTextusesmessages.create(a single non-streaming call, appropriate for the current short-rewrite use case); swap inmessages.stream()from the Anthropic SDK if you build something where perceived latency matters more. - Surfacing usage/cost — nothing currently reads
ai_usageback out; it's write-only today. An admin view summingcostUsd/inputTokensbyfeatureor by day would be a natural/adminaddition, following the same pattern as notifications.md's admin views. - A heavier default model — override per-deployment via
ANTHROPIC_MODEL, or changeDEFAULT_MODELinlib/ai.tsif you want a different default across all deployments of a fork.
How to remove it
Delete lib/ai.ts, lib/actions/ai.ts, the aiUsage table from
lib/db/schema/app.ts (regenerate a migration), the ai_assistant flag
seed in lib/flags.ts, the "Improve with AI" button and its call in
components/marketing/contact-form.tsx, @anthropic-ai/sdk from
package.json, and the two ANTHROPIC_* vars from lib/env.ts.