Skip to main content
skill CX Codex GP ChatGPT official · OpenAI

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.

Install into Codex · Skills CLI (Vercel) All methods and platforms ↓
npx skills add openai/plugins --skill ai-generation-persistence -a codex
Open on GitHub

What 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 codex

2Manually: put the files in place

.agents/skills/ai-generation-persistence/SKILL.md

For all projects use ~/.agents/skills/

From the same set201

The whole set →
supabase official supabase Guides the agent through database configuration and strict RLS policies in Supabase. CX Codex GP ChatGPT 8installs remotion-render official Remotion Exports videos and static stills from Remotion projects using CLI commands. CX Codex GP ChatGPT 3installs figma-create-new-file official Figma Creates blank Figma, FigJam, or Slides files inside draft folders. It resolves planKey and editor types before invoking Figma MCP tools. CX Codex GP ChatGPT 6installs swiftui-performance-audit official OpenAI Pinpoints SwiftUI runtime issues like frame drops, unresponsive taps, and excessive view body computations. Works code-first and guides Instruments profiling. CX Codex GP ChatGPT 109installs swiftui-liquid-glass official OpenAI This skill helps build and review SwiftUI Liquid Glass interfaces for iOS 26+. Use it when adopting the API or checking fallbacks, interaction, and design fit. CX Codex GP ChatGPT 121installs swiftui-ui-patterns official OpenAI This skill helps build and refactor SwiftUI interfaces, from navigation and state to layouts and controls. Use it for new screens or changes to an existing project. CX Codex GP ChatGPT 114installs swiftui-view-refactor official OpenAI Refactors SwiftUI view files into clean, testable structures with dedicated subview types and proper Observation ownership. CX Codex GP ChatGPT 113installs ios-memgraph-leaks official OpenAI Captures and inspects iOS leaks from a simulator process or .memgraph. Useful for tracing retained objects and checking a fix with before-and-after evidence. CX Codex GP ChatGPT 1,054installs

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

See also

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor