ai-generation-persistence — Codex skill: save AI generations
Shows how to store LLM outputs with IDs, metadata, and addressable URLs. Useful when an app needs generation or chat history that survives a page refresh.
npx skills add openai/plugins --skill ai-generation-persistence -a codexWhat ai-generation-persistence does
The skill recommends creating a database record before calling the model, then saving the output and metadata such as model, token usage, estimated cost, timestamp, and user ID. For images and files, its examples store assets in Vercel Blob and put their URLs in the database. Streamed output should be saved too.
The examples use nanoid, Drizzle with Postgres, @vercel/blob, and Upstash Redis. A generate-then-redirect flow sends the user to a page keyed by the generation ID; Redis can cache repeated prompts by model and prompt hash. Those services are prerequisites only if you choose to follow the illustrated setup.
Install it when users need to revisit generated results or open them by URL. Skip these patterns for a project that does not retain generations.
What it helps with
- keep streamed output after a page refresh
- open a saved chat from its ID-based URL
- record token usage and estimated generation cost
- store generated images at permanent URLs
- reuse a cached result for the same model and prompt
Example prompts for the agent
- Add a generation record before calling the model, then show the result at its ID-based URL.
- Save each chat response and its token usage to the database.
- Cache repeated generations by model and prompt.
How to install ai-generation-persistence
1Skills CLI (Vercel)
npx skills add openai/plugins --skill ai-generation-persistence -a codex2Manually: put the files in place
.agents/skills/ai-generation-persistence/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add openai/plugins --skill ai-generation-persistence -a claude-code2Manually: put the files in place
~/.claude/skills/ai-generation-persistence/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/ai-generation-persistence/
Manually: put the files in place
.cursor/skills/ai-generation-persistence/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install openai/plugins ai-generation-persistence2Manually: put the files in place
.github/skills/ai-generation-persistence/SKILL.mdPlatform CLI
gemini skills install https://github.com/openai/plugins --path plugins/vercel/skills/ai-generation-persistenceManually: put the files in place
.devin/skills/ai-generation-persistence/SKILL.mdThe legacy .windsurf/skills/ path still works
Claude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
From the same set201
The whole set →Similar
Questions about ai-generation-persistence
How do I install ai-generation-persistence in Codex?
Run npx skills add openai/plugins --skill ai-generation-persistence. To specify the agent, run npx skills add openai/plugins --skill ai-generation-persistence -a codex.
What does ai-generation-persistence do?
It describes patterns for saving LLM outputs with IDs and metadata, then retrieving them through URLs. It also covers image storage and prompt caching.
Which storage services appear in the examples?
The examples use Neon Postgres with Drizzle for text and history, Vercel Blob for images and files, and Upstash Redis for caching.
Source text
SKILL.md · Apache-2.0
---
name: ai-generation-persistence
description: "AI generation persistence patterns — unique IDs, addressable URLs, database storage, and cost tracking for every LLM generation"
metadata:
priority: 6
docs:
- "https://sdk.vercel.ai/docs/ai-sdk-ui/storing-messages"
sitemap: "https://sdk.vercel.ai/sitemap.xml"
pathPatterns:
- "app/api/generate/**"
- "app/api/generations/**"
- "src/app/api/generate/**"
- "src/app/api/generations/**"
- "app/chat/[id]/**"
- "app/generate/[id]/**"
- "src/app/chat/[id]/**"
- "src/app/generate/[id]/**"
- "lib/generations/**"
- "src/lib/generations/**"
bashPatterns: []
importPatterns:
- "ai"
- "@ai-sdk/*"
- "@vercel/blob"
- "nanoid"
- "@paralleldrive/cuid2"
promptSignals:
phrases:
- "save generations"
- "persist generations"
- "generation history"
- "chat history"
- "save chat"
- "generation id"
- "ai chat"
- "chat app"
- "chatbot"
- "image generation"
- "text generation"
- "ai app"
allOf:
- [generate, save]
- [generate, persist]
- [generate, store]
- [ai, persist]
- [ai, history]
- [ai, database]
- [chat, persist]
- [chat, database]
- [chat, url]
- [generation, url]
- [generation, id]
- [image, generate]
- [stream, save]
- [stream, persist]
anyOf:
- "shareable"
- "retrievable"
- "permalink"
- "cost tracking"
- "token usage"
- "nanoid"
- "cuid"
- "openai"
- "anthropic"
- "llm"
- "gpt"
- "claude"
noneOf:
- "github actions"
- "ci workflow"
minScore: 6
---
# AI Generation Persistence
**AI generations are expensive, non-reproducible assets. Never discard them.**
Every 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.
## Core Rules
1. **Generate an ID before the LLM call** — use `nanoid()` or `createId()` from `@paralleldrive/cuid2`
2. **Persist every generation** — text and metadata to database, images and files to Vercel Blob
3. **Make every generation addressable** — URL pattern: `/chat/[id]`, `/generate/[id]`, `/image/[id]`
4. **Track metadata** — model name, token usage, estimated cost, timestamp, user ID
5. **Never stream without saving** — if the user refreshes, the generation must survive
## Generate-Then-Redirect Pattern
The standard UX flow for AI features: create the resource first, then redirect to its page.
```ts
// app/api/chat/route.ts
import { nanoid } from "nanoid";
import { db } from "@/lib/db";
import { redirect } from "next/navigation";
export async function POST(req: Request) {
const { prompt, model } = await req.json();
const id = nanoid();
// Create the record BEFORE generation starts
await db.insert(generations).values({
id,
prompt,
model,
status: "pending",
createdAt: new Date(),
});
// Redirect to the generation page — it handles streaming
redirect(`/chat/${id}`);
}
```
```tsx
// app/chat/[id]/page.tsx
import { db } from "@/lib/db";
import { notFound } from "next/navigation";
export default async function ChatPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const generation = await db.query.generations.findFirst({
where: eq(generations.id, id),
});
if (!generation) notFound();
// Render with streaming if still pending, or show saved result
return <ChatView generation={generation} />;
}
```
This gives you: shareable URLs, back-button support, multi-tab sessions, and generation history for free.
## Persistence Schema
```ts
// lib/db/schema.ts
import { pgTable, text, integer, timestamp, jsonb } from "drizzle-orm/pg-core";
export const generations = pgTable("generations", {
id: text("id").primaryKey(), // nanoid
userId: text("user_id"), // auth user
model: text("model").notNull(), // "openai/gpt-5.4"
prompt: text("prompt"), // input text
result: text("result"), // generated output
imageUrls: jsonb("image_urls"), // Blob URLs for generated images
tokenUsage: jsonb("token_usage"), // { promptTokens, completionTokens }
estimatedCostCents: integer("estimated_cost_cents"),
status: text("status").default("pending"), // pending | streaming | complete | error
createdAt: timestamp("created_at").defaultNow(),
});
```
## Storage Strategy
| Data Type | Storage | Why |
|-----------|---------|-----|
| Text, metadata, history | Neon Postgres via Drizzle | Queryable, relational, supports search |
| Generated images & files | Vercel Blob (`@vercel/blob`) | Permanent URLs, CDN-backed, no expiry |
| Prompt dedup cache | Upstash Redis | Fast lookup, TTL-based expiry |
## Image Persistence
Never serve generated images as ephemeral base64 or temporary URLs. Save to Blob immediately:
```ts
import { put } from "@vercel/blob";
import { generateText } from "ai";
const result = await generateText({ model, prompt });
// Save every generated image to permanent storage
const imageUrls: string[] = [];
for (const file of result.files ?? []) {
if (file.mediaType?.startsWith("image/")) {
const ext = file.mediaType.split("/")[1] || "png";
const blob = await put(`generations/${generationId}.${ext}`, file.uint8Array, {
access: "public",
contentType: file.mediaType,
});
imageUrls.push(blob.url);
}
}
// Update the generation record with permanent URLs
await db.update(generations)
.set({ imageUrls, status: "complete" })
.where(eq(generations.id, generationId));
```
## Cost Tracking
Extract usage from every generation and store it. This enables billing, budgeting, and abuse detection:
```ts
const result = await generateText({ model, prompt });
const usage = result.usage; // { promptTokens, completionTokens, totalTokens }
const estimatedCostCents = estimateCost(model, usage);
await db.update(generations).set({
result: result.text,
tokenUsage: usage,
estimatedCostCents,
status: "complete",
}).where(eq(generations.id, generationId));
```
## Prompt Dedup / Caching
Avoid paying for identical generations. Cache by content hash:
```ts
import { Redis } from "@upstash/redis";
import { createHash } from "crypto";
const redis = Redis.fromEnv();
function hashPrompt(model: string, prompt: string): string {
return createHash("sha256").update(`${model}:${prompt}`).digest("hex");
}
// Check cache before generating
const cacheKey = `gen:${hashPrompt(model, prompt)}`;
const cached = await redis.get<string>(cacheKey);
if (cached) return cached; // Return cached generation ID
// After generation, cache the result
await redis.set(cacheKey, generationId, { ex: 3600 }); // 1hr TTL
```
## Anti-Patterns
- **Streaming to client without saving** — generation lost on page refresh. Always write to DB as tokens arrive or on completion.
- **Routes without `[id]` segments** — `/api/chat` with no ID means generations aren't addressable. Use `/chat/[id]`.
- **Re-generating identical prompts** — check cache first. Same prompt + same model = same cost for no new value.
- **Ephemeral base64 images** — generated images served inline are lost when the component unmounts. Save to Vercel Blob.
- **Missing metadata** — always store model name, token counts, and timestamp. You need this for cost tracking and debugging.
- **Client-only state** — storing generations only in React state or localStorage. Use a database — generations must survive across devices and sessions.
© Vercel Labs · Apache-2.0 · openai/plugins