Vercel AI SDK skill for Codex

Streaming chat and tools that match your ai version. Codex checks which major of the ai package is installed before writing a line, then builds authenticated streaming routes, useChat UIs that render message parts and tool states, and Zod-typed tools with an explicit step limit. A version table maps the renamed APIs across AI SDK 5, 6 and 7, so upgrades run through codemods and typecheck instead of guesswork.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- vercel-ai-sdk

Installs to ~/.agents/skills/vercel-ai-sdk/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $vercel-ai-sdk, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds TypeScript LLM features with the Vercel AI SDK (the ai package) - streamText and generateText, useChat streaming chat UIs with message parts, tools with Zod inputSchema and multi-step stopWhen loops, structured output via Output.object, provider packages, ToolLoopAgent, telemetry and version migrations between AI SDK 5, 6 and 7. Use when adding a streaming chat route to Next.js, React, Svelte or Vue, defining AI SDK tools, generating typed objects, or upgrading code that uses deprecated AI SDK APIs.

Use it when

  • A TypeScript app needs streaming LLM responses or a chat UI
  • You are defining AI SDK tools or multi-step tool loops
  • Typed objects must come back from a model call
  • Code uses deprecated AI SDK APIs after a major upgrade

Use something else when

How to check the result

Typecheck passes, a curl to the route streams text and tool parts, mock-model tests pass, and stop, error and retry work in the browser.

The mistake it prevents

Pasting examples from a different AI SDK major, or omitting stopWhen so tool results never become a final answer.

~/.agents/skills/vercel-ai-sdk/SKILL.md

--- name: vercel-ai-sdk description: Builds TypeScript LLM features with the Vercel AI SDK (the ai package) - streamText and generateText, useChat streaming chat UIs with message parts,… license: MIT ---

Done well means: the code matches the installed major version of ai, the server route validates input and streams UI message parts, tools have Zod schemas and a step limit, typed output is validated by a schema, keys stay server-side, and the chat UI handles streaming, stop, error and retry states.

Find the version first

npm ls ai @ai-sdk/react @ai-sdk/openai zod 2>/dev/null
node -v

The API changed substantially across majors. Write code for what is installed; upgrade only when asked.

ConceptAI SDK 5AI SDK 6AI SDK 7
Step limitstopWhen: stepCountIs(n)stepCountIs(n)isStepCount(n)
Typed outputgenerateObject / streamObjectgenerateText + output: Output.object(...) (generateObject deprecated)same as 6
System prompt optionsystemsysteminstructions
UI messages to modelconvertToModelMessages(msgs) (sync)await convertToModelMessages(msgs)await
Stream responseresult.toUIMessageStreamResponse()samecreateUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) }) (old helper deprecated)
Agent classExperimental_AgentToolLoopAgentToolLoopAgent
RuntimeNode 18+Node 18+Node 22+, ESM only

When upgrading, run the official codemods for the target major (npx @ai-sdk/codemod upgrade or the version-specific command in the migration guide; needs network), then fix what remains by hand, then run typecheck and tests. Read AGENTS.md for the package manager.

Server route (Next.js App Router, AI SDK 7)

// app/api/chat/route.ts
import {
  streamText, convertToModelMessages, tool, isStepCount,
  createUIMessageStreamResponse, toUIMessageStream, type UIMessage,
} from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

export const maxDuration = 30;

const tools = {
  getOrderStatus: tool({
    description: 'Look up shipping status for an order ID like ORD-1234.',
    inputSchema: z.object({ orderId: z.string().regex(/^ORD-\d+$/) }),
    execute: async ({ orderId }) => orders.status(orderId),   // runs on the server
  }),
};

export async function POST(req: Request) {
  const session = await auth(req);
  if (!session) return new Response('Unauthorized', { status: 401 });

  const { messages }: { messages: UIMessage[] } = await req.json();
  const result = streamText({
    model: openai(process.env.CHAT_MODEL ?? 'gpt-5-mini'),
    instructions: 'You are a support assistant. Use tools for order facts.',
    messages: await convertToModelMessages(messages),
    tools,
    stopWhen: isStepCount(5),
    abortSignal: req.signal,
  });

  return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) });
}

On AI SDK 5/6 the last lines are system: ..., stopWhen: stepCountIs(5) and return result.toUIMessageStreamResponse();.

Server rules:

  • Authenticate before calling the model; rate-limit per user ($api-rate-limiting).
  • Cap history length or tokens before convertToModelMessages; clients can send anything.
  • Never trust tool inputs: the Zod schema validates shape, the execute body must still check the user owns the order.
  • Tools that send, delete, pay or deploy need approval. Recent versions support tool approval (approval-requested UI part states); otherwise render a confirm step client-side and only execute on an explicit user action.

Client (React)

'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Chat() {
  const { messages, sendMessage, status, stop, error, regenerate } = useChat();
  const [input, setInput] = useState('');
  const busy = status === 'submitted' || status === 'streaming';

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          {m.parts.map((part, i) => {
            switch (part.type) {
              case 'text':
                return <p key={i}>{part.text}</p>;
              case 'tool-getOrderStatus':
                return part.state === 'output-available'
                  ? <OrderCard key={i} data={part.output} />
                  : <Spinner key={i} label="Checking order…" />;
              default:
                return null;
            }
          })}
        </div>
      ))}
      {error && <button onClick={() => regenerate()}>Retry</button>}
      <form onSubmit={(e) => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }}>
        <input value={input} onChange={(e) => setInput(e.target.value)} disabled={busy} />
        {busy ? <button type="button" onClick={stop}>Stop</button> : <button type="submit">Send</button>}
      </form>
    </div>
  );
}
  • Render message.parts, not a single content string; tool parts are typed as tool-<toolName> with states such as input-streaming, input-available, output-available and output-error. Handle unknown part types with a default branch.
  • useChat posts to /api/chat by default; configure a transport for other endpoints or extra body fields.
  • Persist chats server-side (save UIMessage[] on finish); see $ai-chat-interface for UX details like markdown rendering and scroll behaviour.

Structured output

import { generateText, Output } from 'ai';

const schema = z.object({
  vendor: z.string(),
  total: z.number().describe('grand total in invoice currency'),
  currency: z.string().length(3),
  dueDate: z.string().nullable().describe('ISO date or null if absent'),
});

const { output } = await generateText({
  model: openai('gpt-5-mini'),
  output: Output.object({ schema }),
  prompt: `Extract invoice fields:\n\n${text}`,
});

On AI SDK 5 use generateObject({ model, schema, prompt }) and read object. For streaming partial objects use streamText with output and iterate partialOutputStream (v6+). Schema design and validation: $structured-extraction.

Agents

ToolLoopAgent packages model, instructions, tools and stop condition for reuse across routes and jobs; its default step limit is higher than streamText’s, so set stopWhen explicitly to the budget you want. Keep plain streamText for a single chat route.

Providers

Install the provider package (@ai-sdk/openai, @ai-sdk/google, @ai-sdk/mistral, …) or route through the AI Gateway with a "provider/model" string if the project already uses it. Keep the model ID in config, not scattered literals, so switching is one change.

Observability

  • AI SDK 7: install @ai-sdk/otel, call registerTelemetry(new OpenTelemetry()) once at startup; telemetry is then on by default and per-call telemetry: { isEnabled: false } opts out.
  • AI SDK 5/6: pass experimental_telemetry: { isEnabled: true, functionId: 'chat' } per call.
  • Record usage from onEnd/onFinish (tokens, model, user) for cost dashboards: $llm-observability.

Gotchas

  • Mixing snippets from different majors is the most common breakage; the table above is the checklist when TypeScript reports a missing export.
  • Without stopWhen, streamText stops after the first step, so tool results never get turned into a final answer.
  • Edge runtime: some provider SDKs and Node APIs are unavailable; prefer the Node runtime unless latency requires edge and dependencies support it.
  • Forgetting abortSignal: req.signal keeps generating (and billing) after the user hits Stop.
  • Zod: keep the version the installed ai peer dependency expects; mismatched Zod majors produce confusing type errors.
  • Never expose provider keys with NEXT_PUBLIC_; all model calls go through the server.

Verification

  • tsc --noEmit and the project’s lint pass.
  • Hit the route locally: curl -N -X POST localhost:3000/api/chat -H 'content-type: application/json' -d '{"messages":[{"id":"1","role":"user","parts":[{"type":"text","text":"status of ORD-1?"}]}]}' and watch the stream include tool and text parts.
  • Unit-test tools’ execute functions directly; test the route with a mock model (MockLanguageModelV2/V3 from ai/test, whichever the installed version exports).
  • In the browser: send, stop mid-stream, trigger an error, retry.

Output summary template

**AI SDK:** ai 7.x, @ai-sdk/react, @ai-sdk/openai; Node 22
**Route:** app/api/chat/route.ts, auth + 40-message cap, isStepCount(5), abort on disconnect
**Tools:** getOrderStatus (read-only), cancelOrder (approval required)
**UI:** parts rendering, stop/retry, tool loading states
**Checks:** tsc clean, 6 tests with mock model, manual stream verified with curl

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Vercel AI SDK"
  short_description: "Streaming chat and tools that match your ai version"
  default_prompt: "$vercel-ai-sdk Add a streaming chat route with tools to our Next.js app using the AI SDK"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem