AI Customization
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- theaiblock: which models are offered, which provider routes requests, credits, logging.aiPlugin({ ... })inchaibuilder.plugins.ts- prompt and tool customization.
Choose which models editors can pick
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.
Restrict which models an action may use
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'],
},
}
Map a model id to a model instance
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 }
},
}
Customize the prompts for actions
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 'chaipro/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:
- Customizers receive the default prompt and return the one to use - append, prepend, or
replace outright. Return
defaultPromptto opt out for a given action. - Branch on
info.action(e.g.AI_EDIT_PAGE) and, where present,info.initiatorfor finer-grained sub-context such asblock/page/TRANSLATE_CONTENT. - Customizers chain - each sees the previous one's result.
- The built-in prompts themselves live in the package and aren't edited in place; this hook is the supported way to change them.
Add your own tools
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.
Use a different AI service
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.providerin config - an explicit AI SDK provider instance or factory.- The first configured provider plugin: your
ai.providers, then the built-ins - OpenRouter, OpenAI-compatible, Cloudflare Workers AI - each auto-activating from its env vars. - Nothing configured - the AI SDK falls back to the Vercel AI Gateway (
AI_GATEWAY_API_KEY).
Built-in providers, by environment variable
| 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.
Plug in any AI SDK provider
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,
}),
}
Ship your own auto-detecting provider plugin
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,
},
],
}
Log every AI request
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),
})
},
},
}
What is not configurable
- The built-in system prompts cannot be edited in place - use
customizePromptto transform them. - Provider auto-detection order is fixed (OpenRouter → OpenAI-compatible → Cloudflare),
though
ai.provideroverrides it entirely.
How to think about it
- Defaults are enough? Add a provider key (Setup) and stop there.
- Need control over models or provider? Configure the
aiblock inchaibuilder.config.ts. - Need control over behavior? Use
aiPlugin({ customizePrompt, customizeTools }).
Related
- AI - Setup - provider gateways and API keys to get running.
- AI Assistant Overview - what the assistant does inside the builder.
- Configuration reference - the full config surface.
- Vercel AI SDK documentation - the SDK the core AI is built on.

