Logo FormaUI

FormaUI

All providers

OpenAI

A complete AI layer for Next.js App Router, built directly on the official openai SDK — no intermediate AI framework. Covers text generation, streaming, structured (Zod-validated) outputs, image generation, embeddings with semantic search, content moderation, and tool calling, with rate limiting and input validation composed in by default.

Dependencies

  • openai: Official SDK — every provider call in service.ts goes through it.
  • zod: Input validation and structured-output schemas.
  • zod-to-json-schema: Converts tool parameter schemas (Zod) to JSON Schema for function calling.
  • server-only: Build-time guard so service.ts/actions.ts never reach the client bundle.

Folder structure

ai/openai/
├── constants.ts               # Models, defaults, limits, retry/rate-limit config, env var names — single source of truth
├── types.ts                   # Shared TypeScript types (AIResult<T>, chat messages, generation params, etc.)
├── errors.ts                  # Normalized AIError class + OpenAI SDK error mapping (keeps the raw provider error code)
├── schemas.ts                 # Zod schemas: input validation + structured-output examples (summary, translation, extraction)
├── rate-limit.ts               # Sliding-window rate limiter, in-memory by default, pluggable store interface
├── service.ts                  # INTERNAL — raw OpenAI SDK calls (client init, generation, streaming, tools, embeddings, moderation)
├── actions.ts                   # PUBLIC — "use server" API: chat, summarize, translate, extractData, askWithTools, image, embed, moderate
├── client.ts                     # Client-safe fetch helper for streaming (the one operation that needs a Route Handler)
├── hooks.ts                       # React hooks for Client Components, built on actions.ts (+ client.ts for streaming)
├── prompts/
│   ├── system.ts                  # Base system prompts (default, summarizer, translator, extractor, tool-using assistant)
│   ├── builder.ts                  # Prompt interpolation, context truncation, message-array assembly
│   ├── summarize.ts                 # Summarization prompt + paired Zod schema
│   ├── translate.ts                  # Translation prompt + paired Zod schema
│   └── extract.ts                     # Entity-extraction prompt + paired Zod schema
├── tools/
│   ├── calculator.ts                  # Example tool — safe arithmetic (no eval/Function)
│   ├── search.ts                       # Example tool — web search via a pluggable provider (Tavily-shaped by default)
│   └── index.ts                         # Tool registry: defaultTools, getToolByName, selectTools
├── routes/
│   └── chat-stream.route.ts              # Exact content to copy to app/api/ai/chat/stream/route.ts (streaming only)
└── env.example

How the files fit together

  • service.ts (internal) talks to the OpenAI API directly. It owns client initialization, retries with backoff, and every raw operation (generateText, streamText, generateStructuredObject, generateImage, generateEmbedding, moderateContent, callWithTools). Nothing here is meant to be imported from a component — it exists to keep provider-specific logic out of the public API.
  • actions.ts (public) is the entry point for everything server-side. Every export is a Server Action: call it directly from a Server Component, a form, or another Server Action — and it can also be imported straight into a Client Component, since Next.js handles that boundary for you. Each action composes service.ts with input validation (schemas.ts) and guardrails (rate limiting + moderation via a RequestContext argument). Every action resolves to AIResult<T> ({ success, data } or { success, error }) instead of throwing.
  • client.ts exists only for streaming — a Server Action can't return a ReadableStream, so streamChat fetches the one bundled Route Handler (routes/chat-stream.route.ts) and exposes the response as text deltas. Everything else in this module never touches fetch.
  • hooks.ts wraps actions.ts (and client.ts for streaming) in React state — loading/data/error handling and stale-response guarding — so Client Components don't manage that by hand.

What actions.ts covers

  • chat — single-turn or multi-turn text generation (non-streaming).
  • askWithTools — full tool-calling round trip (function calling), restricted to registered tool names for safety (toolNames?: string[]).
  • summarize, translate, extractData — structured, schema-validated generation for the three built-in tasks.
  • image — image generation.
  • embed / semanticSearch — embeddings, plus a naive in-memory cosine-similarity ranker for prototyping semantic search.
  • moderate — standalone content moderation check.

Streaming chat is intentionally not here — see routes/chat-stream.route.ts and hooks.ts's useChatStream instead.

Usage examples

Chat (Server Action)

import { chat } from "@/ai/openai/actions";

const result = await chat({ prompt: "Give me a haiku about the sea." }, { userId, plan: "FREE" });

if (result.success) {
  console.log(result.data.text);
} else {
  console.error(result.error.message);
}

Chat — streaming

Copy routes/chat-stream.route.ts to app/api/ai/chat/stream/route.ts once, then consume it from a Client Component:

"use client";
import { useChatStream } from "@/ai/openai/hooks";

function StreamingChat() {
  const { sendMessage, text, isStreaming, stop } = useChatStream();

  return (
    <div>
      <button onClick={() => sendMessage({ prompt: "Write a short story." })}>Ask</button>
      {isStreaming && <button onClick={stop}>Stop</button>}
      <p>{text}</p>
    </div>
  );
}

Tool calling

import { askWithTools } from "@/ai/openai/actions";

const result = await askWithTools({
  prompt: "What is (245 + 15) * 3?",
  toolNames: ["calculator"], // omit to expose every registered tool
});
// result.data => { text, toolCalls, toolResults, usage }

Summarize / translate / extract

import { summarize, translate, extractData } from "@/ai/openai/actions";

const summary = await summarize({ text: article, maxWords: 100 });
const translation = await translate({ text: "Hello world", targetLanguage: "Spanish" });
const extraction = await extractData({ text: invoiceText, entityTypes: ["date", "amount"] });

Images, embeddings, moderation

import { image, embed, semanticSearch, moderate } from "@/ai/openai/actions";

const picture = await image({ prompt: "a lighthouse at sunset, watercolor style" });
const vector = await embed({ input: "Next.js App Router" });
const matches = await semanticSearch("refund policy", knowledgeBaseRecords, { topK: 3 });
const check = await moderate("some user-submitted text");

Client Components — hooks

"use client";
import { useChat } from "@/ai/openai/hooks";

function ChatBox() {
  const { sendMessage, data, isLoading, error } = useChat();

  return (
    <div>
      <button onClick={() => sendMessage({ prompt: "Give me a haiku about the sea." })}>Ask</button>
      {isLoading && <p>Loading…</p>}
      {error && <p>{error.message}</p>}
      {data && <p>{data.text}</p>}
    </div>
  );
}

useImageGeneration, useEmbedding, and useAskWithTools follow the same { data, error, isLoading, run } shape.

What you can build with this

This module is a complete foundation for any AI-powered feature in a Next.js app: a support chatbot (with or without tool calling to look things up), a "summarize this document" or "translate this page" button, an AI-assisted search box (embeddings + semantic search), automatic content moderation on user-generated text, structured data extraction from freeform text (invoices, forms, support tickets), or an image-generation feature for user avatars, thumbnails, or creative tools — all without writing any OpenAI SDK code, prompt-assembly logic, or rate-limiting/ moderation plumbing by hand.