← VercelCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
changed
Update to Vercel
Snapshot Sep 30, 2026 · 23:18 UTC · version 0.21.4
Collection source: not recorded for this historical snapshot. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.
Supporting file metadata differs
Newly listed paths: agents/openai.yaml. This compares saved file lists, not package contents; a different collection source can change the list.
Observed in package metadata. These changes alone do not establish a new customer-facing feature.
Supporting files
Before
[]
After
[{"relative_path":"agents/openai.yaml","size_in_bytes":190}]
Compare saved observations
Download comparison JSONFull technical diff · 1 changed fields
changed /included_files
BEFORE
[]
AFTER
[
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 190
}
]Full snapshot data
{
"name": "ai-generation-persistence",
"description": "AI generation persistence patterns — unique IDs, addressable URLs, database storage, and cost tracking for every LLM generation",
"included_files": [
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 190
}
],
"skill_md_contents": "---\nname: ai-generation-persistence\ndescription: \"AI generation persistence patterns — unique IDs, addressable URLs, database storage, and cost tracking for every LLM generation\"\nmetadata:\n priority: 6\n docs:\n - \"https://sdk.vercel.ai/docs/ai-sdk-ui/storing-messages\"\n sitemap: \"https://sdk.vercel.ai/sitemap.xml\"\n pathPatterns:\n - \"app/api/generate/**\"\n - \"app/api/generations/**\"\n - \"src/app/api/generate/**\"\n - \"src/app/api/generations/**\"\n - \"app/chat/[id]/**\"\n - \"app/generate/[id]/**\"\n - \"src/app/chat/[id]/**\"\n - \"src/app/generate/[id]/**\"\n - \"lib/generations/**\"\n - \"src/lib/generations/**\"\n bashPatterns: []\n importPatterns:\n - \"ai\"\n - \"@ai-sdk/*\"\n - \"@vercel/blob\"\n - \"nanoid\"\n - \"@paralleldrive/cuid2\"\n promptSignals:\n phrases:\n - \"save generations\"\n - \"persist generations\"\n - \"generation history\"\n - \"chat history\"\n - \"save chat\"\n - \"generation id\"\n - \"ai chat\"\n - \"chat app\"\n - \"chatbot\"\n - \"image generation\"\n - \"text generation\"\n - \"ai app\"\n allOf:\n - [generate, save]\n - [generate, persist]\n - [generate, store]\n - [ai, persist]\n - [ai, history]\n - [ai, database]\n - [chat, persist]\n - [chat, database]\n - [chat, url]\n - [generation, url]\n - [generation, id]\n - [image, generate]\n - [stream, save]\n - [stream, persist]\n anyOf:\n - \"shareable\"\n - \"retrievable\"\n - \"permalink\"\n - \"cost tracking\"\n - \"token usage\"\n - \"nanoid\"\n - \"cuid\"\n - \"openai\"\n - \"anthropic\"\n - \"llm\"\n - \"gpt\"\n - \"claude\"\n noneOf:\n - \"github actions\"\n - \"ci workflow\"\n minScore: 6\n---\n\n# AI Generation Persistence\n\n**AI generations are expensive, non-reproducible assets. Never discard them.**\n\nEvery call to an LLM costs real money and produces unique output that cannot be exactly reproduced. Treat generations like database records — assign an ID, persist immediately, and make them retrievable.\n\n## Core Rules\n\n1. **Generate an ID before the LLM call** — use `nanoid()` or `createId()` from `@paralleldrive/cuid2`\n2. **Persist every generation** — text and metadata to database, images and files to Vercel Blob\n3. **Make every generation addressable** — URL pattern: `/chat/[id]`, `/generate/[id]`, `/image/[id]`\n4. **Track metadata** — model name, token usage, estimated cost, timestamp, user ID\n5. **Never stream without saving** — if the user refreshes, the generation must survive\n\n## Generate-Then-Redirect Pattern\n\nThe standard UX flow for AI features: create the resource first, then redirect to its page.\n\n```ts\n// app/api/chat/route.ts\nimport { nanoid } from \"nanoid\";\nimport { db } from \"@/lib/db\";\nimport { redirect } from \"next/navigation\";\n\nexport async function POST(req: Request) {\n const { prompt, model } = await req.json();\n const id = nanoid();\n\n // Create the record BEFORE generation starts\n await db.insert(generations).values({\n id,\n prompt,\n model,\n status: \"pending\",\n createdAt: new Date(),\n });\n\n // Redirect to the generation page — it handles streaming\n redirect(`/chat/${id}`);\n}\n```\n\n```tsx\n// app/chat/[id]/page.tsx\nimport { db } from \"@/lib/db\";\nimport { notFound } from \"next/navigation\";\n\nexport default async function ChatPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n const generation = await db.query.generations.findFirst({\n where: eq(generations.id, id),\n });\n if (!generation) notFound();\n\n // Render with streaming if still pending, or show saved result\n return <ChatView generation={generation} />;\n}\n```\n\nThis gives you: shareable URLs, back-button support, multi-tab sessions, and generation history for free.\n\n## Persistence Schema\n\n```ts\n// lib/db/schema.ts\nimport { pgTable, text, integer, timestamp, jsonb } from \"drizzle-orm/pg-core\";\n\nexport const generations = pgTable(\"generations\", {\n id: text(\"id\").primaryKey(), // nanoid\n userId: text(\"user_id\"), // auth user\n model: text(\"model\").notNull(), // \"openai/gpt-5.4\"\n prompt: text(\"prompt\"), // input text\n result: text(\"result\"), // generated output\n imageUrls: jsonb(\"image_urls\"), // Blob URLs for generated images\n tokenUsage: jsonb(\"token_usage\"), // { promptTokens, completionTokens }\n estimatedCostCents: integer(\"estimated_cost_cents\"),\n status: text(\"status\").default(\"pending\"), // pending | streaming | complete | error\n createdAt: timestamp(\"created_at\").defaultNow(),\n});\n```\n\n## Storage Strategy\n\n| Data Type | Storage | Why |\n|-----------|---------|-----|\n| Text, metadata, history | Neon Postgres via Drizzle | Queryable, relational, supports search |\n| Generated images & files | Vercel Blob (`@vercel/blob`) | Permanent URLs, CDN-backed, no expiry |\n| Prompt dedup cache | Upstash Redis | Fast lookup, TTL-based expiry |\n\n## Image Persistence\n\nNever serve generated images as ephemeral base64 or temporary URLs. Save to Blob immediately:\n\n```ts\nimport { put } from \"@vercel/blob\";\nimport { generateText } from \"ai\";\n\nconst result = await generateText({ model, prompt });\n\n// Save every generated image to permanent storage\nconst imageUrls: string[] = [];\nfor (const file of result.files ?? []) {\n if (file.mediaType?.startsWith(\"image/\")) {\n const ext = file.mediaType.split(\"/\")[1] || \"png\";\n const blob = await put(`generations/${generationId}.${ext}`, file.uint8Array, {\n access: \"public\",\n contentType: file.mediaType,\n });\n imageUrls.push(blob.url);\n }\n}\n\n// Update the generation record with permanent URLs\nawait db.update(generations)\n .set({ imageUrls, status: \"complete\" })\n .where(eq(generations.id, generationId));\n```\n\n## Cost Tracking\n\nExtract usage from every generation and store it. This enables billing, budgeting, and abuse detection:\n\n```ts\nconst result = await generateText({ model, prompt });\n\nconst usage = result.usage; // { promptTokens, completionTokens, totalTokens }\nconst estimatedCostCents = estimateCost(model, usage);\n\nawait db.update(generations).set({\n result: result.text,\n tokenUsage: usage,\n estimatedCostCents,\n status: \"complete\",\n}).where(eq(generations.id, generationId));\n```\n\n## Prompt Dedup / Caching\n\nAvoid paying for identical generations. Cache by content hash:\n\n```ts\nimport { Redis } from \"@upstash/redis\";\nimport { createHash } from \"crypto\";\n\nconst redis = Redis.fromEnv();\n\nfunction hashPrompt(model: string, prompt: string): string {\n return createHash(\"sha256\").update(`${model}:${prompt}`).digest(\"hex\");\n}\n\n// Check cache before generating\nconst cacheKey = `gen:${hashPrompt(model, prompt)}`;\nconst cached = await redis.get<string>(cacheKey);\nif (cached) return cached; // Return cached generation ID\n\n// After generation, cache the result\nawait redis.set(cacheKey, generationId, { ex: 3600 }); // 1hr TTL\n```\n\n## Anti-Patterns\n\n- **Streaming to client without saving** — generation lost on page refresh. Always write to DB as tokens arrive or on completion.\n- **Routes without `[id]` segments** — `/api/chat` with no ID means generations aren't addressable. Use `/chat/[id]`.\n- **Re-generating identical prompts** — check cache first. Same prompt + same model = same cost for no new value.\n- **Ephemeral base64 images** — generated images served inline are lost when the component unmounts. Save to Vercel Blob.\n- **Missing metadata** — always store model name, token counts, and timestamp. You need this for cost tracking and debugging.\n- **Client-only state** — storing generations only in React state or localStorage. Use a database — generations must survive across devices and sessions.\n"
}SHA-256: 670e1ee4d5280173f2ad431475e184316faec3fb778b7230eb97617f08ab0224