AI SDK & Gateway
Use Vercel AI SDK with AI Gateway for multi-provider AI features.
Use Vercel AI SDK with AI Gateway for multi-provider AI features.
Architecture Overview
lib/
├── ai.ts # Gateway setup, getModel(), re-exports from 'ai'
├── ai-credits.ts # Credit tracking and consumption
└── actions/ai.ts # Server actions for non-streaming operations
hooks/
└── use-ai.ts # React hooks for client components
app/api/
├── chat/route.ts # Streaming chat endpoint (default for useChat)
└── ai/models/ # Available models endpointConfiguration
All AI settings in config/app.ts:
aiConfig = {
enabled: true,
defaultModel: "openai/gpt-4o-mini",
models: [...], // Allowed models
creditCosts: { chat: 1, completion: 1, generation: 2 },
defaults: { maxTokens: 1024, temperature: 0.7 },
}Environment variables in .env:
AI_GATEWAY_BASE_URL=https://gateway.ai.cloudflare.com/v1/...
AI_GATEWAY_API_KEY=your-api-keyUsing AI in Server Code
Basic Text Generation
import { getModel, generateText } from "@/lib/ai";
const model = getModel("openai/gpt-4o"); // or use default
const result = await generateText({
model,
prompt: "Write a tagline for a SaaS product",
});
console.log(result.text);
console.log(result.usage); // { inputTokens, outputTokens }Streaming Response
import { gateway, streamText, convertToModelMessages, type UIMessage } from "@/lib/ai";
// Convert UI messages to model format
const messages: UIMessage[] = [...];
const modelMessages = await convertToModelMessages(messages);
const result = streamText({
model: gateway("openai/gpt-4o"),
messages: modelMessages,
maxOutputTokens: 1024,
temperature: 0.7,
});
// For API routes returning to useChat
return result.toUIMessageStreamResponse();With Credit Checking
import { getModel, generateText } from "@/lib/ai";
import { canUseCredits, consumeCredits } from "@/lib/ai-credits";
import { aiConfig } from "@/config/app";
// Check credits first
const cost = aiConfig.creditCosts.completion;
if (!(await canUseCredits(userId, planName, cost))) {
throw new Error("Insufficient AI credits");
}
const model = getModel();
const result = await generateText({ model, prompt });
// Deduct credits after success
await consumeCredits(userId, cost, "completion", {
model: aiConfig.defaultModel,
provider: "openai",
promptTokens: result.usage.inputTokens,
completionTokens: result.usage.outputTokens,
});Using AI in Client Components
Streaming Chat (Primary Method)
Use useChat from @ai-sdk/react for streaming conversations:
"use client";
import { useChat } from "@ai-sdk/react";
export function ChatComponent() {
const { messages, sendMessage, status, stop, setMessages } = useChat();
const handleSubmit = (text: string, modelId: string) => {
sendMessage(
{ text },
{ body: { modelId } } // Pass model selection to API
);
};
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong>
{/* Messages use parts array */}
{m.parts.map((part, i) => (
part.type === "text" ? <span key={i}>{part.text}</span> : null
))}
</div>
))}
{status === "streaming" && <span>AI is typing...</span>}
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(inputValue, "openai/gpt-4o");
}}>
{/* input handling */}
</form>
</div>
);
}Non-Streaming Generation
Use server actions for one-off generation:
"use client";
import { useGenerateText } from "@/hooks/use-ai";
export function GeneratorComponent() {
const { execute, status, result } = useGenerateText();
return (
<div>
<button
onClick={() => execute({ prompt: "Write a haiku about coding" })}
disabled={status === "executing"}
>
{status === "executing" ? "Generating..." : "Generate"}
</button>
{result?.data?.text && <p>{result.data.text}</p>}
</div>
);
}Checking Credits
"use client";
import { useAICredits } from "@/hooks/use-ai";
import { useEffect } from "react";
export function CreditDisplay() {
const { execute, result } = useAICredits();
useEffect(() => {
execute();
}, [execute]);
if (!result?.data) return null;
const { used, limit, remaining, isUnlimited } = result.data;
return (
<div>
{isUnlimited ? (
<span>Unlimited credits</span>
) : (
<span>{remaining} / {limit} credits remaining</span>
)}
</div>
);
}Model Selection
"use client";
import { useAvailableModels } from "@/hooks/use-ai";
export function ModelSelector({ value, onChange }) {
const { models, isLoading, error } = useAvailableModels();
if (isLoading) return <span>Loading models...</span>;
if (error) return <span>Failed to load models</span>;
return (
<select value={value} onChange={(e) => onChange(e.target.value)}>
{models.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option>
))}
</select>
);
}Server Actions
Server actions in lib/actions/ai.ts handle auth and credits automatically:
import { generateTextAction, chatAction, getCreditsAction } from "@/lib/actions/ai";
// Non-streaming text generation
const result = await generateTextAction({
prompt: "Write a summary",
systemPrompt: "You are a helpful assistant",
model: "openai/gpt-4o", // optional
});
// Non-streaming chat
const response = await chatAction({
messages: [{ role: "user", content: "Hello" }],
model: "openai/gpt-4o",
});
// Get credit status
const credits = await getCreditsAction();API Routes
Streaming Chat Endpoint
POST /api/chat - Default endpoint for useChat hook
// Request body
{
messages: UIMessage[],
modelId?: string // defaults to aiConfig.defaultModel
}
// Response: UI message stream (SSE)Models Endpoint
GET /api/ai/models - Returns available models
// Response
{
models: [
{ id: "openai/gpt-4o", name: "GPT-4o" },
{ id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
...
],
defaultModel: "openai/gpt-4o-mini"
}Credit System
Credits reset monthly per user. Tracked in ai_usage table.
| Operation | Cost |
|---|---|
| Chat message | 1 credit |
| Text completion | 1 credit |
| Structured generation | 2 credits |
Plan limits set in stripeConfig.plans[].limits.aiCredits:
-1= unlimited0= no AI access100= 100 credits/month
Adding New Models
- Add to
aiConfig.modelsinconfig/app.ts:
models: [
"openai/gpt-4o",
"anthropic/claude-sonnet-4-20250514",
"google/gemini-1.5-pro",
// Add new model here
"openai/gpt-4-turbo",
]- Model is immediately available via gateway
Model Format
Models use provider/model format for AI Gateway:
- OpenAI:
openai/gpt-4o,openai/gpt-4o-mini - Anthropic:
anthropic/claude-sonnet-4-20250514,anthropic/claude-3-5-haiku-20241022 - Google:
google/gemini-1.5-pro,google/gemini-1.5-flash
Error Handling
AI errors are automatically captured to Sentry. User-facing errors:
- 402: Insufficient credits - prompt upgrade
- 400: Invalid model - check aiConfig.models
- 401: Unauthorized - user not logged in
- 500: AI provider error - retry or fallback
// Hooks handle errors automatically with toast notifications
const { execute } = useGenerateText();
// useChat also handles errors
const { error } = useChat();
if (error) {
// Error displayed, also available for custom handling
}Testing
Visit /ai-test for a demo chat interface that shows:
- Streaming messages
- Model selection
- Error handling
- Status indicators
For more details, see the official AI SDK documentation.