AI
AI Setup
Configure Vercel AI SDK models in the VitNode API config, declare what each model can do and what it costs, then use them with c.get("ai").
VitNode integrates the Vercel AI SDK. Register models in vitnode.api.config.ts once, then use them inside any API handler via c.get("ai").
Quick start
1. Configure models in the API config
A model can be an AI Gateway id string or a direct provider instance:
import { openai } from '@ai-sdk/openai'
import { buildApiConfig } from '@vitnode/core/vitnode.config'
export const vitNodeApiConfig = buildApiConfig({
ai: {
models: [
{
id: 'default',
name: 'GPT-4o Mini',
model: openai('gpt-4o-mini'),
capabilities: ['text', 'structured-output', 'streaming'],
pricing: {
rates: { inputPerMillion: '0.15', outputPerMillion: '0.60' },
},
},
],
},
})Set OPENAI_API_KEY in your .env file.
capabilities and pricing are optional, but AI actions rely on them: capabilities decide which model can run an action, and pricing turns token usage into money. See Model capabilities and Costs, Points & Budgets.
2. Use it in a route handler
For user-facing features, run a registered AI action. It checks permissions, limits and budgets, and records the cost:
handler: async (c) => {
const { output } = await c.get('ai').run({
action: EXCERPT_AI_ACTION,
input: c.req.valid('json'),
})
return c.json({ text: output })
}For internal tooling, you can also resolve the raw model and call the SDK directly. See AI Usage.
Model capabilities
A capability says what a configured language model can do. VitNode never guesses it from the model's name. A model without capabilities is treated as ["text"].
| Capability | Meaning |
|---|---|
text | Text in, text out. |
image-input | Accepts images in the prompt (vision). |
structured-output | Returns reliable JSON objects (Output.object). |
streaming | Streams tokens. Required by c.get("ai").stream() and Quick Ask. |
Every AI action declares the capabilities it needs. When a run starts, VitNode picks the first configured model that has all of them. If no model fits, the run fails with AI_MODEL_INCOMPATIBLE before anything is sent to a provider.
image-input means the model can read images, which features such as image
ALT text need. Generating images is a different thing: register those models
under imageModels and resolve them with c.get("ai").imageModel(). Add
image-input only to models whose provider docs say they accept images.
Otherwise VitNode keeps images away from them.
Model pricing
pricing is what the model costs on this connection, in USD per one million tokens. Prices are decimal strings, so money never goes through floating point:
{
id: 'default',
name: 'Claude Sonnet',
model: anthropic('claude-sonnet-4-5'),
capabilities: ['text', 'image-input', 'structured-output', 'streaming'],
pricing: {
rates: {
inputPerMillion: '3',
outputPerMillion: '15',
cacheReadPerMillion: '0.30',
cacheWritePerMillion: '3.75',
},
},
}Prices live only in this config. The AdminCP has no price editor, so a price change is a code change you deploy. A malformed price stops the server at boot.
Without pricing, a call's cost stays unknown, never zero. Under a budget cap, a model with no pricing is refused with AI_PRICING_MISSING. Tiers, flat fees and gateway vs direct pricing are covered in Costs, Points & Budgets.
Model options
Prop
Type
ai also accepts providerAdapters: readers for providers that report their own cost. The AI Gateway and OpenRouter readers are built in. See Provider-reported cost.
Multiple models & embeddings
Configure dedicated models for fast completions, reasoning, and vector embeddings:
ai: {
models: [
{
id: 'default',
name: 'GPT-4o Mini',
model: openai('gpt-4o-mini'),
capabilities: ['text', 'structured-output', 'streaming'],
},
{
id: 'reasoning',
name: 'Claude Sonnet',
model: 'anthropic/claude-sonnet-5', // AI Gateway id
capabilities: ['text', 'image-input', 'structured-output', 'streaming'],
},
],
embeddingModels: [
{ id: 'default', name: 'Text Embedding 3', model: openai.embedding('text-embedding-3-small') },
],
}Resolve specific models by ID:
const smartModel = c.get('ai').model('reasoning')
const embedModel = c.get('ai').embeddingModel()
const capabilities = c.get('ai').capabilities('reasoning') // ['text', 'image-input', ...]model(), embeddingModel(), imageModel() and models() work as before. Admins can assign a different model (and a fallback) to each action in the AdminCP; an assigned model that lacks a required capability is refused.
Reading the model list in the browser
Provider instances, API keys and prices stay on the server. c.get("ai").models() returns only the public half of each entry:
| Field | Value |
|---|---|
id | The configured id, e.g. "default". |
name | The configured label. |
model | The provider's model id string, e.g. "gpt-4o-mini". |
provider | "gateway" for AI Gateway ids, otherwise the SDK model's provider. |
capabilities | The declared capabilities, or ["text"]. |
The middleware route publishes this list, and the browser reads it with
useMiddlewareConfigQuery():
import { useMiddlewareConfigQuery } from '@vitnode/core/tanstack/auth'
export const ModelPicker = () => {
const { data } = useMiddlewareConfigQuery()
if (data.ai.models.length === 0) return <p>AI is not configured.</p>
return (
<select className="w-full">
{data.ai.models.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
)
}The first entry is the default, which c.get("ai").model() resolves when a
handler gets no id. The list is empty when no ai.models are configured, so
a picker can hide itself instead of offering nothing.
Learn more
AI Actions
Managed AI features with permissions, limits and history
Costs, Points & Budgets
Pricing, AI points, budgets and what happens on failure
AdminCP & Account Usage
Budgets, actions, role access, history and the member's /settings/ai page
Automatic Image ALT Text
Background ALT text in every site language, under a budget
AI in Fields & the Editor
AI suggestions on content fields, Quick Ask and translation freshness
AI Usage
Streaming text, structured outputs, and embeddings
Search & Discovery
Semantic vector search across plugin collections