
Made with ChaiBuilder
The AI assistant's default behavior is a starting point, not a ceiling. Its core is written with the open-source Vercel AI SDK, so anything compatible with the Vercel AI SDK will work — with some coding changes. This page covers the common customizations. If you only need the assistant running with defaults, see AI — Setup first.
Two places to make changes:
chaibuilder.config.ts — the ai block: which models are offered, which provider
routes requests, credits, logging.aiPlugin({ ... }) in chaibuilder.plugins.ts — prompt and tool customization.The ai.models array populates the model picker in the builder's AI panel. Each entry is a
model id in provider/model form plus display metadata.
// chaibuilder.config.ts
const chaiConfig = buildChaiBuilderConfig({
// …
ai: {
credits: { enabled: false },
models: [
{
id: 'google/gemini-3-flash',
name: 'Gemini 3 Flash',
provider: 'google',
multiplier: 1,
description: '1x Credits',
},
{
id: 'anthropic/claude-sonnet-4.5',
name: 'Claude Sonnet 4.5',
provider: 'anthropic',
multiplier: 3,
description: '3x Credits',
},
{
id: 'zai/glm-5.2',
name: 'GLM 5.2',
provider: 'zai',
multiplier: 3,
description: '3x Credits',
// text-only model — it cannot read images/files
allowedFileTypes: [],
},
],
},
})
Omit ai.models and you get the built-in list. The built-in default model is
google/gemini-3-flash — override it with ai.defaultModelId.
ai.actionModels maps a model id to the AI actions it is allowed to handle:
ai: {
defaultModelId: 'google/gemini-3-flash',
actionModels: {
'anthropic/claude-sonnet-4.5': ['AI_EDIT_PAGE'],
'google/gemini-3-flash': ['AI_GENERATE_THEME', 'AI_GENERATE_SEO_FIELD'],
},
}
By default a model id is passed through to the AI SDK as a bare string. Use ai.resolveModel
to intercept that — return a model instance and any provider options, keyed on the model id
and the action asking for it:
import { createAnthropic } from '@ai-sdk/anthropic'
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
ai: {
resolveModel: (modelId, aiActionName) => {
if (modelId.startsWith('anthropic/')) {
return {
model: anthropic(modelId.replace('anthropic/', '')),
providerOptions: aiActionName === 'AI_EDIT_PAGE' ? { anthropic: { thinking: { type: 'enabled' } } } : undefined,
}
}
return { model: modelId }
},
}
Every AI action builds its own system prompt, then runs it through any registered customizers
before handing it to the model. Register yours through aiPlugin:
// chaibuilder.plugins.ts
import { aiPlugin } from '@chaibuilder/pro/plugins'
export const chaiServerPlugins = [
// …
aiPlugin({
customizePrompt: (defaultPrompt, info) => {
// info: { action, initiator?, appId?, userId?, data? }
if (info.action === 'AI_GENERATE_SEO_FIELD') {
return `${defaultPrompt}
Meta titles must stay under 60 characters and descriptions under 155.
Use the page's primary keyword once, naturally. No clickbait.`
}
if (info.action === 'AI_EDIT_PAGE') {
return `${defaultPrompt}
House rules: keep headings under 20 words. Never invent product claims.
Prefer existing design tokens over new inline styles.`
}
return defaultPrompt // leave every other action alone
},
}),
]
Notes on how this behaves:
defaultPrompt to opt out for a given action.info.action (e.g. AI_EDIT_PAGE) and, where present, info.initiator for
finer-grained sub-context such as block / page / TRANSLATE_CONTENT.Tool-driven actions such as AI_EDIT_PAGE accept extra AI SDK tools. Tools with an
execute() run server-side inside the model's tool loop:
import { tool } from 'ai'
import { z } from 'zod'
aiPlugin({
customizeTools: (info) => {
if (info.action !== 'AI_EDIT_PAGE') return {}
return {
lookupProduct: tool({
description: 'Look up a product by SKU to get accurate pricing and copy.',
inputSchema: z.object({ sku: z.string() }),
execute: async ({ sku }) => await getProductBySku(sku),
}),
}
},
})
Returned tool maps are merged onto the action's base tools, with your keys winning.
Model ids are handed to the Vercel AI SDK as provider/model strings. Which provider actually
serves them is resolved in this order:
globalThis.AI_SDK_DEFAULT_PROVIDER, if your app set it — ChaiBuilder never overwrites it.ai.provider in config — an explicit AI SDK provider instance or factory.ai.providers, then the built-ins — OpenRouter,
OpenAI-compatible, Cloudflare Workers AI — each auto-activating from its env vars.AI_GATEWAY_API_KEY).| Provider | Environment variables |
|---|---|
| Vercel AI Gateway (fallback) | AI_GATEWAY_API_KEY |
| OpenRouter | OPENROUTER_API_KEY, optional OPENROUTER_APP_NAME, OPENROUTER_APP_URL |
| OpenAI-compatible | OPENAI_COMPATIBLE_BASE_URL, OPENAI_COMPATIBLE_API_KEY, optional OPENAI_COMPATIBLE_NAME |
| Cloudflare Workers AI | CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN |
Provider packages are optional peer dependencies imported lazily — install only the one you use.
ai.provider takes any object exposing the AI SDK model factory methods, or a factory
returning one. This covers providers with no built-in adapter — Bedrock, Hugging Face, a
custom gateway:
// chaibuilder.config.ts
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
ai: {
provider: () =>
createOpenAICompatible({
name: 'self-hosted',
baseURL: 'https://llm.internal.example.com/v1',
apiKey: process.env.INTERNAL_LLM_API_KEY,
}),
}
For an "install the package, set the env var" experience, add a plugin to ai.providers. It's
tried ahead of the built-ins and activates itself when isConfigured() passes:
ai: {
providers: [
{
id: 'my-gateway',
label: 'My Gateway',
isConfigured: () => Boolean(process.env.MY_GATEWAY_KEY),
createProvider: async () => {
const { createMyGateway } = await import('my-gateway-ai-provider')
return createMyGateway({ apiKey: process.env.MY_GATEWAY_KEY })
},
// rebuild the provider when the key rotates
fingerprint: () => process.env.MY_GATEWAY_KEY,
},
],
}
ai.logging.logger receives each request after it completes — use it for cost tracking or
auditing:
ai: {
logging: {
clientId: 'acme-prod',
logger: async ({ userId, model, prompt, response, error, startTime, creditStatus }) => {
await db.insert(aiRequestLog).values({
userId,
model,
durationMs: Date.now() - startTime,
failed: Boolean(error),
})
},
},
}
customizePrompt to
transform them.ai.provider overrides it entirely.ai block in
chaibuilder.config.ts.aiPlugin({ customizePrompt, customizeTools }).