ChaiBuilder Logo

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 - the ai block: which models are offered, which provider routes requests, credits, logging.
  • aiPlugin({ ... }) in chaibuilder.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 defaultPrompt to opt out for a given action.
  • Branch on info.action (e.g. AI_EDIT_PAGE) and, where present, info.initiator for finer-grained sub-context such as block / 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:

  1. globalThis.AI_SDK_DEFAULT_PROVIDER, if your app set it - ChaiBuilder never overwrites it.
  2. ai.provider in config - an explicit AI SDK provider instance or factory.
  3. 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.
  4. 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 customizePrompt to transform them.
  • Provider auto-detection order is fixed (OpenRouter → OpenAI-compatible → Cloudflare), though ai.provider overrides 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 ai block in chaibuilder.config.ts.
  • Need control over behavior? Use aiPlugin({ customizePrompt, customizeTools }).

© ChaiBuilder. All rights reserved.