← VercelCONTENT HISTORY

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.

WHAT CHANGED · RULE-BASED ANALYSIS

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 JSON
Full 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