- an autoresponder for every text message your commands, triggers and flows did not handle, with a per-chat history;
ctx.tools.ai, a client you call from your own handlers, flows and conversations;- optional tool calling, long-term memory and RAG and streaming.
Quickstart (autoresponder)
Install the plugin and the SDK of your provider (here Anthropic, the default):- A catch-all autoresponder (
bot.onMessage) that answers any text message no command, trigger or conversation consumed. It keeps a per-chat history window (20 messages by default,context.maxMessages) in the bot’s store. - A
/resetcommand that clears the chat’s AI history and cancels its active conversation (turn it off withresetCommand: false).
WhatsApp AI policy (since 2026-01-15)
What that means foraiPlugin:
- Write a
personascoped to your business (what you sell, opening hours, policies) that politely declines anything unrelated, as in the quickstart. - Offer a way to reach a human (handoff) and don’t market the bot as an “ask me anything” assistant.
- The rule comes from the official Cloud API terms (
@botmesh/meta). Baileys is an unofficial WhatsApp Web client and is also subject to WhatsApp’s terms.
Choose a provider
Every provider supports tool calling (on Ollama and the presets, when the
model does) and streaming.
Default models are not covered by semverProviders retire models on their own schedule, so the default models follow the
current ones and can change in a minor release. Set
model explicitly when the
choice matters to you (cost, latency, output style).Anthropic (default)
baseUrl points the SDK at a gateway or proxy. For high-volume chat a smaller
model (claude-sonnet-5-5, claude-haiku-5-5) is cheaper and faster.
OpenAI
baseUrl targets any OpenAI-compatible server (vLLM, LM Studio, a gateway) or
Azure OpenAI. The output-token cap is sent as max_completion_tokens to OpenAI
itself and to Azure OpenAI (api.openai.com, *.openai.azure.com,
*.cognitiveservices.azure.com, or *.services.ai.azure.com under
/openai), and as max_tokens to any other baseUrl, which is what those
servers expect. maxTokensField overrides the choice, for example for a proxy
in front of OpenAI:
Gemini
Install@google/genai. The older @google/generative-ai SDK is deprecated by
Google; it is still used when @google/genai is missing, with a one-time
DeprecationWarning. baseUrl becomes the SDK’s httpOptions.baseUrl.
aiPlugin does not expose, build the
provider yourself and pass it as instance:
Ollama (local or cloud)
Ollama runs open models on your own machine. No SDK and no API key: the plugin talks to its HTTP API withfetch.
- Tool calling needs a model trained for it (Llama 3.1+, Qwen 3, Mistral and the others tagged tools in the Ollama library).
-
No output cap by default:
maxTokensis sent asoptions.num_predictonly when you set it, so thinking models are not cut off mid-reasoning. -
Cloud models through your local Ollama: sign in once
(
ollama signin) and use a cloud model name (the ones ending in-cloud, such asgpt-oss:120b-cloud) with the same config. Ollama forwards the request to its cloud; your bot still talks tolocalhost. -
Ollama’s cloud API directly (no local server), or an Ollama behind a
proxy with auth: set
apiKeyand it is sent asAuthorization: Bearer <key>(never logged, never in an error message):
Presets
DeepSeek, Qwen, GLM, Kimi, Groq, OpenRouter, Mistral, Grok and Together are OpenAI-compatible: the plugin reaches them through theopenai SDK with their
own base URL, so openai is the only package to install. Each preset has a
default model and reads its key from its usual environment variable:
apiKey, model and baseUrl override the preset’s values.
Supported SDK versions
The SDKs are optional peer dependencies; install only the one your provider needs.
The tests drive each SDK at both ends of its range against a local fake of the
HTTP API: plain replies, tool calls with their results, and streaming.
Use the AI from your code: ctx.tools.ai
The plugin also does bot.provide('ai', client), so ctx.tools.ai is
available in any handler, flow or conversation. Turn the autoresponder off
(autoReply: false) if that is all you want:
ctx.tools.ai is an AIClient:
Typing (TypeScript). Importing
@botmesh/ai augments the core’s
DefaultTools, so ctx.tools.ai (and bot.injectTools.ai outside handlers)
is typed as AIClient with no casts. The type does not check that the plugin
is installed: without bot.plug(aiPlugin(...)), or after unplugging it,
ctx.tools.ai is undefined at runtime.
Thinking (Anthropic)
Claude models think before answering.@botmesh/ai picks the right request
for the model you configure (thinking: 'auto', the default):
'adaptive'always sends adaptive thinking;'off'never sends the parameter (models that always think, such as Claude Opus 5.5, keep doing so: lowereffortinstead).{ budgetTokens }(a positive integer; anything else throws aRangeErrorwhen the config is built) enables budgeted thinking (budget_tokens, raised to the minimum of 1024) on the models without adaptive thinking (Haiku 4.5, Sonnet 4.5, Opus 4.5 and older). Adaptive models get adaptive thinking instead: Opus / Sonnet 4.6 still accept budgets but deprecate them, and Opus 4.7+, Sonnet 5+, Haiku 5+, Fable and Mythos reject them. WhenmaxTokenswould leave no room for the answer it becomesbudgetTokens + maxTokens.- If the API still rejects the request with a 400 that names
thinking(oreffort), the call is retried once without that parameter and the model is remembered, so the next calls skip it.anthropicModelSupport(model)exposes the rules.
Tools, memory, RAG and streaming
Each has its own guide; this is what they look like in the plugin config. Tool calling: the model calls your functions (search the catalog, place an order), with argument validation, timeouts, a human confirmation forconfirm: true tools and audit events.
memory remembers facts about each
user across conversations (stored in ctx.user); rag answers from your
documents with numbered citations, through any retriever, such as createRag()
from @botmesh/rag.
stream: true sends the
autoreply while it is generated: native drafts on Telegram, live edits on
Discord, sentence chunks with “typing…” on WhatsApp. Turns with tools are not
streamed.
Formatting per channel
Models write Markdown (**bold**, # heading, [link](url), - item), but
most chat apps do not render it. The autoresponder converts each reply (also
when streaming) to the format of the chat, ctx.textFormat: the provider’s
per-chat textFormatFor(chatId) when it serves several channels, else its
capabilities.textFormat.
Code spans and fenced blocks are never touched, and neither are
snake_case
words or 2 * 3. The stored history keeps the model’s original text.
format: 'whatsapp' (or any format) forces one; format: false sends the text
as is. formatMarkdown(text, format) and formatMarkdownStream(source, format)
are exported for your own handlers. With 'html' only http(s), mailto: and
tel: URLs become <a href> links: anything else (a javascript: link a
prompt injection could produce) stays as text.
Voice notes
The autoresponder answers text messages only. With the@botmesh/voice plugin, voice notes are transcribed before
routing (use autoTranscribe: { asText: true } so the AI answers them) and
autoReplyWithVoice can send the answer back as audio.
Language of the built-in replies
The texts the plugin writes for you (the error and refusal replies, the tool confirmation prompt and its Yes/No buttons, the sources line) follow the chat’s language,ctx.lang. English and Spanish are built in; English is the
default. A Spanish-only bot sets lang: 'es' on the Bot:
bot.locale() under the ai. keys (ai.error,
ai.refusal, ai.tools.confirm…, listed in AI_MESSAGES), or set
errorMessage, refusalMessage and maxStepsMessage on the plugin. See
i18n.
/reset and slashCommandsPlugin
aiPlugin registers its own /reset: it clears the chat’s AI context and
cancels the active conversation. Command registration is “last plug()
wins”, so if you also use slashCommandsPlugin (which ships its own /reset
to cancel conversations), install aiPlugin after it so its /reset
takes precedence:
Cost and latency
Every autoreply turn is one paid model call (more with tools). To keep the bill and the wait down:- Answer the obvious without AI. Commands,
bot.when()triggers and flows run before the autoresponder; FAQs, menus and greetings handled there never reach the model. - Pick the model per job. A small model is usually enough for support chat;
give the background memory extraction a cheaper one
(
memory: { autoExtract: { model: '...' } }). - Cap the output.
maxTokenslimits each answer (it includes the thinking tokens of reasoning models); on Anthropic,effort: 'low'thinks less. - Send less history. Each turn resends the chat window: lower
context.maxMessagesfor short exchanges. With RAG, fewer and smaller passages (topK,chunkSize) mean a shorter prompt. - Bound the tool loop. Each step is one model call: keep
maxStepslow and give tools precise descriptions so the model calls the right one first. - Stay quiet in groups.
groupMode: 'mentions'on theBotmakes the bot answer group messages only when mentioned. - Feel faster.
stream: trueshows the answer as it is written;humanizeTyping(on by default) shows “typing…” meanwhile. - Protect your quota. Rate limiting caps how many
messages a chat can push through, and
timeoutMsbounds each call. - Measure. With tracing, every model call is a span with its model and token usage.
Errors and troubleshooting
aiPlugin never crashes the process over a misconfigured provider (SDK not
installed, unknown provider, missing API key): install is synchronous and
resolves the provider lazily. At startup a warmup logs the problem as an error,
and each message then gets the errorMessage reply instead of an answer.
A configuration that can never work fails fast instead: aiPlugin() throws a
RangeError / TypeError naming the option for a timeout that is not a finite
number of ms (a NaN from Number(process.env.X) would fire after 1 ms), a
maxTokens that is not a positive integer, a negative or NaN
context.maxMessages, invalid memory limits, an unparsable
toolConfirm.timeout or a tool without handler.
bot.stop() aborts every model call still in flight: the autoreply turns,
background memory extraction and the calls your own handlers make through
ctx.tools.ai, which reject with an AbortError. A slow provider never keeps
the process alive. A call that exceeds timeoutMs rejects with
AITimeoutError.
Full aiPlugin(config) reference
The default texts are the English ones; a chat in Spanish gets the Spanish
built-ins (see Language of the built-in replies).
Testing
MockAIProvider (from @botmesh/ai/testing) implements AIProvider with
fixed or scripted replies and no network calls. Plug it into the real plugin
with instance:
AIClient takes the provider and a ContextManager over any
ConvStore (the bot’s store is one):
reply can also be a function of the request that returns { toolCalls } to
script tool calls (see Testing in the tool calling guide),
and mock.calls records every request the client sent.
The package has three entries:
A custom
AIProvider that streams should honour GenerateInput.onStreamEnd:
call it once, after the last chunk, only when the stream ends normally (not on
an error, an abort or an early break), with an AIStreamEndInfo
(stopReason, usage when known). Check signal.aborted before calling it: a
client that ignores the signal would otherwise end “normally” after an abort.
Limitations
- The history is a fixed window of messages (
context.maxMessages); older turns are dropped, not summarized. - One persona per plugin: a bot that needs several picks them in its own
handlers with
ctx.tools.ai.generate({ system, messages }). - The autoresponder answers text only (see Voice notes).
Next steps
- AI tool calling — let the model call your bot’s functions
- AI memory & RAG — per-user facts and answers from your documents
- Streaming replies —
ctx.stream()per channel - Voice — transcribe voice notes and answer with audio
- Handoff — hand the chat to a human
- Plugins — how
bot.plug()works in general