Configuration¶
VibeBot uses Limnoria's registry for all configuration. See the Limnoria configuration documentation for how the registry works in general.
Set values from IRC with:
View a current value by omitting the value. Channel-overridable settings also accept a per-channel form:
Scope in the tables below means: channel settings accept per-channel overrides; global settings apply everywhere.
A new default in a release does not change a running bot
Limnoria writes the whole registry to bot.conf when the bot shuts down, so every setting the bot has ever loaded exists there as an explicit line. That line wins over the default shipped in a later release.
So when an upgrade changes a Default in the tables below, the new value applies to fresh installs only. An existing deployment keeps whatever was persisted, silently and with no error — the feature simply behaves as it did before.
To adopt a new default, set it explicitly with @config (which writes through the registry), or stop the bot, edit bot.conf, and start it again. Editing bot.conf while the bot runs does not work: the next shutdown flush overwrites it.
Verify by reading back the value with @config plugins.LLM.<settingName> — a green deploy and a quiet log do not prove the new value took effect.
API keys¶
API keys are not part of the Limnoria registry. They come from environment variables, one per provider, read at the point each provider call is made and selected by the provider of the model being called — not by which command or channel triggered the call. The old assistantApiKey / codeApiKey / imageApiKey / searchApiKey registry settings were removed, not deprecated: @config on them now errors.
| Variable | Provider |
|---|---|
XAI_API_KEY |
xAI (Grok) |
GEMINI_API_KEY |
Google Gemini |
OPENAI_API_KEY |
OpenAI |
ANTHROPIC_API_KEY |
Anthropic (Claude) |
Any other provider LiteLLM recognises — vertex_ai, openrouter, azure, bedrock, and so on — resolves to no key from this plugin, so LiteLLM falls back to that provider's own native credentials: Application Default Credentials, IAM, or its own environment variables. With the defaults, a fresh install needs ANTHROPIC_API_KEY for chat and @code, XAI_API_KEY for search and @draw (the default imageModel is xai/grok-imagine-image).
Running on local models¶
A local server (Ollama, vLLM, LM Studio) takes no key from this plugin. Set its address in the env file and pick models with the matching LiteLLM prefix:
| Server | Env variable | Model prefix |
|---|---|---|
| Ollama | OLLAMA_API_BASE=http://host:11434 |
ollama_chat/<model> |
| vLLM | HOSTED_VLLM_API_BASE=http://host:8000/v1 |
hosted_vllm/<model> |
| LM Studio | LM_STUDIO_API_BASE=http://host:1234/v1 |
lm_studio/<model> |
Don't use openai/<model> with OPENAI_API_BASE for a local server. That redirects every OpenAI call in the bot and sends OPENAI_API_KEY to your box.
The chat path calls tools, so the local model must support function calling. Each role has its own model setting (assistantModel, searchModel, codeModel, memoryModel, verseModel and the rest under Model selection). Any role left on a hosted provider still sends its requests there. That matters when you turn off privacyOptInOnly: keep every model a channel uses local, or overheard lines can still reach a hosted provider.
Opting in to Vertex AI¶
Vertex AI is not the default, but it remains a supported imageModel (and general model) choice for anyone who wants it — for example to bill image generation to a separate GCP project. Credentials (ADC, IAM) are enough for authentication, but LiteLLM's Vertex Imagen path separately requires the project and region, and it fails closed with vertex_project and vertex_location are required for Vertex AI if they are missing:
| Variable | Required | Notes |
|---|---|---|
VERTEXAI_PROJECT |
Yes | Your GCP project ID |
VERTEXAI_LOCATION |
Yes | Region, e.g. us-central1. VERTEX_LOCATION also works as a fallback name |
Vertex AI takes no bearer key from this plugin — no VERTEX_AI_API_KEY exists; it authenticates through ADC and IAM only.
Drawing against a self-hosted endpoint¶
imageApiBase points @draw at any OpenAI-shaped /v1/images/generations endpoint — including the same box @animate uses, which serves images as well as video. Set it and three things change: the request carries an api_base, it authenticates with ANIMATE_API_KEY instead of the provider key imageModel's name would imply, and imageSteps/imageSize ride along in the request body. Leave it empty and nothing changes; the hosted provider path is untouched.
@config channel #chan supybot.plugins.LLM.imageApiBase http://host:port/v1
@config channel #chan supybot.plugins.LLM.imageModel openai//path/to/model
@config channel #chan supybot.plugins.LLM.imageSteps 8
@config channel #chan supybot.plugins.LLM.imageSize 1024x576
The openai/ prefix tells LiteLLM which wire format to speak; everything after it is the model name your server expects, which is why a filesystem path with slashes in it is fine.
Two things to weigh before switching a busy channel over:
- Steps are the whole latency budget. Measured against the reference box at 1024x576 on an idle server: 8 steps ≈ 32 seconds, 25 steps ≈ 94 seconds. Compare a hosted provider answering in a few seconds. Raise
drawTimeout(default 120) to cover whatever you pick, or draws will fall into the stash-and-deliver-later path. - A busy box is much slower than an idle one. That server runs jobs concurrently rather than queueing, so a draw submitted while clips are rendering can take well over twice as long, and every other job on the box slows down too.
Spend is recorded as $0.00, which is accurate for your own hardware — and unlike an unpriced hosted model, it does not log the "no price in IMAGE_COST_PER_IMAGE" warning, so that warning still means what it says.
Falling back when a provider refuses¶
The same endpoint can sit behind a hosted provider instead of replacing it. Set imageFallbackApiBase and a refusal on content grounds is redrawn against your own box rather than reaching the user as an error:
@config channel #chan supybot.plugins.LLM.imageFallbackApiBase http://host:port/v1
@config channel #chan supybot.plugins.LLM.imageFallbackModel openai//path/to/model
@config channel #chan supybot.plugins.LLM.imageFallbackSteps 8
@config channel #chan supybot.plugins.LLM.imageFallbackSize 1024x576
The fallback is sent the original prompt, never a safety rewrite. A rewrite exists to talk a filter around, and an endpoint with no filter has nothing to be talked around — so the picture that arrives is the one that was asked for, and it carries no 🔁 marker because nothing was reworded.
This interacts with drawAutoRewriteMax (default 1), and the two orderings buy different things:
drawAutoRewriteMax |
What a refused prompt does |
|---|---|
1 (default) |
Rewrites once and redraws on the provider. If that is refused too, the fallback draws the original. Fast when it works, but the user gets a softened version of their request, and the rewrite costs a second billed call |
0 |
Skips rewriting and goes straight to the fallback. Slower per refusal (a self-hosted draw is tens of seconds), free, and faithful to what was asked |
Refusals are still recorded either way. A recovered draw carries its blocked_attempts through to the usage table, so the refusal rate stays measurable, and the fallback logs image_fallback_served at WARNING so you can count how often it earns its keep.
If the fallback also fails — refused again, misconfigured, or the box is down — the user gets the original refusal message. Which of two backends disappointed them is not something they asked about.
Video generation¶
@animate talks to a self-hosted vLLM video server rather than a LiteLLM provider, so it needs both halves of its own credential and stays off until it has them:
| Setting | Where | Notes |
|---|---|---|
ANIMATE_API_KEY |
Environment | Bearer token for the video server. Covered by log redaction like every other *_API_KEY |
animateApiUrl |
Registry | Base URL, e.g. http://videohost:14205 — no trailing /v1. Empty (the default) disables @animate and hides the generate_video tool |
With either half missing the command returns a configuration error and the tool never reaches the model, so it cannot promise a clip the bot has no way to render.
@animate never sends the words the user typed. A planner turn rewrites the ask into a script the video server can render, so the usage row records both: prompt holds the request and rendered_prompt holds what was submitted. Every other command leaves rendered_prompt empty. Rows written before this landed have it empty too — the rendered wording was only ever held in pending_tasks, which is cleared as each clip is delivered.
Copy .env.example to ~/.config/vibebot/env and set the variables there; the systemd unit passes that file to the container with --env-file (see Installation). A model whose provider has no key configured fails with an error naming the missing variable, for example no API key configured for provider 'xai' (set XAI_API_KEY).
Model selection¶
Models follow LiteLLM's provider/model format. Model names are validated against LiteLLM's known providers; the bot rejects unrecognised names and suggests alternatives.
| Setting | Scope | Default | Description |
|---|---|---|---|
assistantModel |
channel | anthropic/claude-haiku-5-5 |
All assistant text and tool work: chat, planner, memory, reminders, scheduled tasks. Needs vision support if users paste image URLs |
refusalFallbackModel |
channel | xai/grok-4-1-fast-non-reasoning |
Re-runs a chat turn once when its model refuses: a policy refusal that survives its one retry, or a provider refusal error. The fallback gets the original request without the refusal. Empty turns it off. Skipped when the turn already runs on this model, when its provider has no key, or when a tool that changes something (a reminder, an image, a memory) already worked this turn, since the re-run would repeat it. Its cost books at xai/grok-4.3 rates, the model behind the alias |
codeModel |
channel | anthropic/claude-haiku-5-5 |
The code-generation one-shot behind @code and the generate_code tool. The @code planner loop itself runs on assistantModel |
imageModel |
channel | xai/grok-imagine-image |
Model for @draw |
imageApiBase |
channel | empty | Draw against an OpenAI-shaped endpoint of your own instead of the provider imageModel names — see Drawing against a self-hosted endpoint |
imageSteps |
channel | 0 |
Denoising steps for a self-hosted endpoint. 0 lets the server choose. Ignored unless imageApiBase is set |
imageSize |
channel | empty | Output geometry as WxH for a self-hosted endpoint. Ignored unless imageApiBase is set |
imageFallbackApiBase |
channel | empty | Endpoint to redraw against when the primary provider refuses a prompt — see Falling back when a provider refuses |
imageFallbackModel |
channel | empty | Model for imageFallbackApiBase. A plain string, not a validated model name |
imageFallbackSteps |
channel | 0 |
Denoising steps for the fallback endpoint |
imageFallbackSize |
channel | empty | Output geometry as WxH for the fallback endpoint |
searchModel |
channel | xai/grok-4.3 |
Model for web search and URL fetch. Falls back to assistantModel. Only xAI and Gemini models search; any other provider answers from its own knowledge. GitHub links skip it: they are read through GitHub's API. A request can run at most three searches |
memoryModel |
channel | empty | Model for the memory extraction that runs after every reply. Falls back to assistantModel. A flash-lite class model is enough |
subjectResearchEnabled |
channel | True |
Research the real people, places and events a @draw or @animate request names before the planner writes the prompt — see Subject research |
subjectResearchModel |
channel | empty | Model for the subject-research pre-stage. Falls back to searchModel, then assistantModel |
verseModel |
channel | empty | Model for verse-mode replies. Falls back to assistantModel. Set this when the assistant model is a terse reasoning model that writes poor prose |
verseCompactionModel |
global | anthropic/claude-haiku-5-5 |
Cheap model for the daily verse compaction job. Unlike the channel model keys this one is a plain string, so a misspelled model name is accepted at @config time and only fails when the nightly job runs |
Channel overrides let different channels run different models:
@config channel #dev plugins.LLM.assistantModel anthropic/claude-sonnet-4-20250514
@config channel #casual plugins.LLM.assistantModel anthropic/claude-haiku-5-5
Subject research¶
Image and video generators know nothing by name. @animate renders a bare
"Churchill" as nothing at all, or as the word printed across the frame, and
@draw is little better — which is why both commands run a planner turn that
rewrites the request before the generator ever sees it. The planner is told to
keep the name the user typed and put a description of the subject beside it,
but for a real person or a real event it has nothing to build that description
from: the verse canon block covers characters the channel invented, not people
the world already contains.
subjectResearchEnabled (on by default) closes that gap with one grounded
completion in front of the planner. It identifies the real subjects the request
names, looks up how each actually looks, and layers the answer onto the
planner's prompt. A request that names nobody real — "a slow aerial shot over a
pine forest" — comes back with nothing and adds nothing to the prompt.
The cost is one extra call and a few seconds. @animate hides that behind a
render measured at 135 seconds; @draw does not, so @draw gets visibly
slower with this on. The typing indicator covers the wait. The usage table
records research costs under the command name dossier, separately from the
planner's own row, because the two run on different models.
Only the dash-prefixed fact lines the research prompt asks for are kept, and
only those that end in a (source: …) tag. A model that declines the
question, or wraps its answer in commentary, contributes nothing rather than
contributing its refusal to the next model's prompt. The source tag guards the
other failure: asked about a name that exists nowhere, a grounded model guesses
an appearance from the words in it rather than answering NONE, and a guess
has nothing to cite. The tag is stripped before the line reaches the planner.
subjectResearchModel picks the model. Left empty it falls back to
searchModel and then assistantModel, which in most deployments means the
researcher is a different provider from the planner. Set it when the search
model is more reticent about describing real people than the generator
downstream is.
System prompts¶
System prompts define the bot's personality and constraints. Both are channel-overridable.
| Setting | Description |
|---|---|
assistantSystemPrompt |
Personality and constraints for all assistant work. Verse mode layers scene context on top of this prompt rather than replacing it |
codeSystemPrompt |
Instructions for @code output format |
Both defaults carry constraints worth keeping: assistantSystemPrompt holds replies to one to three lines of plain text with no markdown, and codeSystemPrompt keeps the code body in the tool's URL rather than in the channel. Read the shipped text before you replace it — @config default plugins.LLM.assistantSystemPrompt — because a replacement drops those constraints with it.
Example: give a channel a specialised personality:
@config channel #python plugins.LLM.assistantSystemPrompt You are a Python expert. Answer with Python examples. Be concise.
Verse¶
The verse is the per-channel world model. See The verse for concepts and operation; this table is the key reference.
| Setting | Scope | Default | Description |
|---|---|---|---|
verseEnabled |
channel | False |
Main switch. When off, verse commands are unavailable and no events are recorded |
verseTriggerRegex |
channel | \bverse\b |
Case-insensitive pattern that marks a message as a canon reference, alongside entity mentions. A match grounds the reply in canon and, for an avatar holder, turns it into a prose tale. It does not enter roleplay mode; that needs @rp. Empty disables the keyword signal |
verseRoleplayStickyTtlSeconds |
channel | 900 |
Sliding inactivity timeout for sticky @rp on roleplay. Each in-character turn refreshes it. 0 never expires |
verseChatRecordEnabled |
channel | False |
Let an opted-in avatar's ordinary chat turn call verse_record. Off keeps canon growing only during roleplay |
verseEventRetentionDays |
channel | 30 |
Events older than this are eligible for compaction. 0 disables compaction for the channel |
verseAutoEntityRetireDays |
channel | 14 |
Days without a mention before auto-created non-player characters (NPCs) retire. 0 disables the sweep. Pinned entities are exempt |
verseAutoEntityMaxNamesPerCall |
channel | 8 |
Cap on the actors array in one verse_record call |
verseRosterMaxChars |
channel | 4000 |
Character cap on the canon-roster block in the verse system prompt |
verseStyleExemplars |
channel | [] |
Curated style exemplars injected into the verse prompt. Populate offline with the taste miner |
verseReactionCaptureEnabled |
channel | True |
Capture IRCv3 emoji reactions to verse lines as an offline approval signal |
verseCompactionDailyAt |
global | 03:00 |
Local HH:MM time for the daily compaction sweep. Empty falls back to 03:00. A malformed value does not — it defers the next run by an hour, each time, and logs a warning |
verseCompactionMinKeepEvents |
global | 20 |
Verses with fewer total events are skipped by compaction |
Storybook¶
These caps cover both illustrated-page paths: the verse_storybook tool on verse turns, and the standalone @story command. @story needs no verse — it is gated like @draw, on llm.draw plus an authenticated account — so these limits still bind in a channel where verseEnabled is False. The verse prefix is historical; renaming the keys would orphan existing bot.conf values.
| Setting | Scope | Default | Description |
|---|---|---|---|
verseStorybookEnabled |
channel | False |
Expose the verse_storybook tool on verse turns. It does not gate @story |
verseStorybookMaxImages |
channel | 5 |
Cap on illustrations per storybook |
verseStorybookMaxPerTurn |
channel | 1 |
Cap on storybooks per verse turn. Tool-only; @story does not read it |
verseStorybookCooldownSeconds |
channel | 300 |
Per-account cooldown between storybooks |
verseStorybookDailyImageCap |
channel | 30 |
Registered but not yet enforced — there is no per-account daily image count to check it against |
verseStorybookMaxChars |
channel | 6000 |
Character cap on the storybook body |
verseStorybookImageTimeout |
channel | 45 |
Seconds to wait for each illustration |
Turning verseStorybookEnabled off does not stop image costs on its own, because @story still runs. What bounds them today is verseStorybookMaxImages per page and the per-account cooldown between pages, not the daily cap.
Memory¶
| Setting | Scope | Default | Description |
|---|---|---|---|
memoryEnabled |
channel | True |
Enable automatic memory extraction |
memoryMaxPerUser |
global | 50 |
Cap on durable memories per user |
memoryCleanupInterval |
global | 3 |
Extraction passes between cleanup runs. 0 disables |
memoryPromotionThreshold |
global | 2 |
Mentions a candidate fact needs before promotion. 1 restores single-stage saving |
memoryCandidateTTLDays |
global | 14 |
Days an unreinforced candidate survives. 0 disables pruning |
See Memory promotion for how the two-stage pipeline works.
Conversation context¶
| Setting | Scope | Default | Description |
|---|---|---|---|
contextEnabled |
channel | True |
Enable conversation context |
contextMaxMessages |
channel | 20 |
Messages kept in per-user history |
contextTimeoutMinutes |
channel | 5 |
Clear context after this much inactivity |
privacyOptInOnly |
channel | True |
Send only lines addressed to the bot to the provider. Set False to track every channel message as context and let irc_lookup read the channel's server history (kind=history, up to 100 lines) for "what did I miss". Those lines go to whichever provider serves the channel's models. See Running on local models |
channelContextMaxMessages |
channel | 10 |
Messages in the shared channel context |
Rate limiting¶
Rate limits follow the pattern {command}{tier}RateLimitCount and {command}{tier}RateLimitWindow for the commands ask, code, draw, story, and animate. All rate-limit settings are global. See Rate limiting and security for tiers, defaults, and the observe-only switch enforceRateLimits.
Bot-to-bot loops¶
Two bots that answer each other never get bored, so replies to another bot are capped.
| Setting | Scope | Default | Description |
|---|---|---|---|
botLoopReplyLimit |
channel | 3 |
Consecutive replies to one bot before the bot goes quiet. 0 disables the guard |
botLoopWindow |
channel | 300 |
Seconds of quiet after which that bot's count resets |
botLoopHostSuffixes |
global | Bot.AfterNET.Org |
Host suffixes the network reserves for bots. Matched case-insensitively against the sender's host. A sender matched here, or one with the +B mode, cannot address the bot by replying to its lines. A full host works too, for a bot on a user host |
A nick counts as a bot only when the network says so, by either of two signals. AfterNET advertises BOT=B and marks flagged users in the WHOX status field, which is where the bot reads it — six nicks carry it today, long-running service bots among them. Anyone unflagged is treated as a person and is never capped, and a human addressing the bot clears the counts in that channel, so a conversation someone has joined is never cut short. Only addressed lines reset it: what people say during a bot loop is commentary on the noise ("wtf", "jesus"), and treating that as a reset would license three more replies after every such line.
The +B mode is opt-in, and the bot most likely to loop with yours may not set it — grok answers on Hxz from grok.Bot.AfterNET.Org. So the sender's host counts too: AfterNET only issues *.Bot.AfterNET.Org to registered bots, and across a full day of channel logs the only nicks on one were grok, grook5, and vibebot itself. The host is checked first because it is free and decides on the very first line, with no WHO round trip.
Because skipAutoWhoOnJoin suppresses the WHO on join, the flag is fetched on demand: the first time an unknown nick addresses the bot, a WHO goes out for that nick and the answer is cached for an hour. That first line is always answered — the guard fails open, so the worst case is one extra reply, never a silenced user. A bot that never sets +B is invisible to this and will not be capped.
Web and HTTP output¶
| Setting | Scope | Default | Description |
|---|---|---|---|
httpRoot |
global | empty | Filesystem path for output files. Empty keeps them under Limnoria's data/web/llm/ and serves them from its built-in HTTP server. Setting it unhooks that server — an external server such as nginx must serve the path, and httpUrlBase must be set to match, or every generated link 404s |
httpUrlBase |
global | empty | Public base URL for output files. Empty uses Limnoria's publicUrl plus /llm; if publicUrl is also unset the bot falls back to http://localhost:<http port>/llm, which nobody off the host can open |
imageUploadUrl |
global | empty | External image host for generated images. Empty stores them locally |
helpUrl |
global | https://rdrake.github.io/vibebot-v8/ |
URL shown in help output |
longReplyTeaserMaxChars |
channel | 220 |
Characters in the one-line teaser for long replies saved as HTML |
multilineMaxLines |
channel | 3 |
Replies that wrap to at most this many IRC lines go inline as one IRCv3 draft/multiline batch instead of a teaser and link. Needs supybot.protocols.irc.experimentalExtensions. 0 turns it off |
fileCleanupAge |
global | 720 |
Delete output files older than this many hours (30 days) |
fileCleanupMax |
global | 1000 |
Cap on files kept in the output directory |
Offloading images to an external host¶
Set imageUploadUrl to an uploader that takes a multipart POST on the images[] field and answers with JSON:
Generated images then live on that host instead of under httpRoot, and storybook pages embed the absolute URL. Pages themselves (answers, code, stories) still come from your own HTTP server, so this offloads bandwidth rather than replacing it.
Every failure falls back to local storage: endpoint unreachable, upload rejected, image over 10 MB, or a reply naming a URL that is not an image on the configured host. An outage costs a slower draw, nothing more.
paste.boxlabs.uk/img/ is an externally operated uploader, the host this contract follows, and it needs no key. Ask the host's owner before pointing the bot at somebody else's uploader.
Limnoria tool bridge¶
| Setting | Scope | Default | Description |
|---|---|---|---|
bridgeEnabled |
channel | False |
Expose loaded Limnoria plugin commands to the LLM as a tool |
bridgeAllowedPlugins |
channel | empty | Plugin allowlist. Empty uses the curated read-safe default set |
bridgeAllowMutating |
channel | False |
Expose state-changing commands through the bridge |
bridgeScheduledTaskLimit |
channel | 5 |
Active scheduled LLM tasks per creator per channel. 0 disables scheduling |
bridgeDebugInChannel |
channel | False |
Append a bridge-call debug footer to replies |
ircLookupEnabled |
channel | True |
Advertise the irc_lookup tool (live LIST, NAMES, WHOIS, WHO, topic, network info, CTCP) to chat. Its history kind also needs privacyOptInOnly off. @channels and @names work regardless |
memeModel |
channel | anthropic/claude-haiku-5-5 |
Model that picks a template and writes captions when @meme (or make_meme's brief) names no template the catalog knows. It opens on a short list (the 40 most popular templates plus up to 20 sharing a word with the request, about 5K prompt tokens) and can call a search_templates tool up to twice to reach the rest of the catalog. No search: one call, about 5K tokens, 2 s. Each search is another round trip: about 12K tokens and 4 s with one, 19K and 6 s with two. Under $0.003 on Haiku either way. Empty falls back to assistantModel; grok-4-1-fast copies the template's example captions instead of writing new ones |
memeEditModel |
global | xai/grok-imagine-image |
Image model for the picker's draw (and make_meme's). Templates with picture slots get one generated picture per slot (/v1/images/generations), masked into the blank; other templates send the captioned meme to /v1/images/edits. xai/ only. About $0.02 per picture, refusals billed too |
memeEnabled |
channel | True |
False refuses @meme and hides the make_meme tool from chat |
memeAliases |
global | tyrone=yallgot biggums=yallgot chappelle=yallgot |
Extra template names, name=id or name=https://image. An id target adds the name as a keyword on that template; a URL target adds a two-caption template drawn on that image. Hyphens for spaces in the name |
See Tuning and monitoring for the bridge's security model and the scheduled-task tool.
Status pages¶
| Setting | Scope | Default | Description |
|---|---|---|---|
statusPageUrls |
global | Claude=https://status.claude.com GitHub=https://www.githubstatus.com OpenAI=https://status.openai.com |
Space-separated status pages to poll and announce — both Atlassian Statuspage and incident.io pages work — each written as Name=url or as a bare url, which takes its host as its name. Names are 1-32 chars of [A-Za-z0-9._-], case-insensitively unique across this key and statusQueryablePages, and are what the model uses to ask about one service. Unusable entries are dropped with a warning and the rest survive; at most 5 are polled |
statusQueryablePages |
global | empty | Same Name=url grammar as statusPageUrls, for pages the bot can be asked about but never polls or announces — the long tail like Cloudflare or AWS, where polling forever to answer an occasional question is the wrong trade. Fetched lazily on request and cached for 5 minutes, with a failure backoff. An entry naming the same page as a statusPageUrls entry is dropped, whatever its name — that page is already reachable, polled, as the fresher entry. At most 20 |
statusAnnounce |
channel | False |
Announce incidents from every configured statusPageUrls page in this channel as they open and again as they resolve. statusQueryablePages pages never announce, no matter how this is set |
check_service_status is on the model's tool surface whenever either key holds at least one page. An empty statusPageUrls no longer removes the tool by itself — only polling and announcing stop; a non-empty statusQueryablePages keeps the tool available for on-request lookups.
See Tuning and monitoring for the polling rotation, staleness, and the RSS cutover order.
Aurora¶
| Setting | Scope | Default | Description |
|---|---|---|---|
auroraEnabled |
channel | True |
Give the chat model the aurora tool: NOAA aurora probability, solar wind, Kp, darkness, and cloud cover for a named place |
auroraAnnounce |
channel | False |
Announce G3+ storm watches and sustained storm conditions in this channel. Works without auroraEnabled |
auroraAnnouncePlaces |
channel | empty | Optional space-separated places for this channel's announcements; _ stands for a space, as in Sault_Ste._Marie. Adds cloud cover for them and holds a storm line until one is dark. A place that matches nothing is skipped (warned once in the log) |
The tool reads NOAA SWPC (services.swpc.noaa.gov) and Open-Meteo (geocoding-api.open-meteo.com, api.open-meteo.com). No API keys are needed. The tool and the announcer share one cache: solar wind for 2 minutes, aurora probability for 10 minutes, storm alerts for 5 minutes, the Kp forecast for 3 hours, cloud cover for 1 hour per place (a 5-day forecast), and place lookups for 24 hours (misses for 1 hour). A failed fetch is not cached. See Aurora.
Announcements go to every channel with auroraAnnounce on; set auroraAnnouncePlaces to add cloud cover and hold storm alerts until it is dark there. The bot checks every 5 minutes and posts two kinds of line, both fixed templates with no model rewrite:
- Storm watch: when NOAA's newest geomagnetic storm watch puts a UTC day at G3 or higher. A re-issued watch at the same level is not announced again; a rise (G3 to G4) is. With places set, the line adds "Clouds night of
": the mean over each place's remaining dark hours in the local night that overlaps that UTC day most, or the next night if that one is already over. - Storm conditions: once per episode of Bz at or below -10 nT for most of 3 hours. With places set, the line waits until at least one of them is dark, and cloud figures are the mean over the next 3 hours; without places it goes out at once with no cloud figures. A place that matches nothing is skipped (warned once in the log); if a lookup service outage stops every place resolving, the storm line waits for the next check.
The first check after a restart, and the first check after a channel turns announcements on, only records what is already in effect; nothing already running is announced. A channel whose send fails is retried on the next check. If no channel announces, nothing is fetched.
Reminders and scheduled tasks¶
| Setting | Scope | Default | Description |
|---|---|---|---|
pendingTasksEnabled |
channel | False |
Advertise set_reminder and schedule_llm_task to the model, so natural-language scheduling works. Off by default: the schemas and their rules cost roughly 1,100 prompt tokens on every completion in the channel |
The @remind command and the firing of reminders and tasks already created work regardless. This key only controls whether the model can create reminders and scheduled tasks from plain language. Listing and cancelling are not on the chat tool surface at any setting: users reach both their reminders and their scheduled tasks with @remind list, del and clear.
Timeouts and retries¶
When an API call times out, the bot retries in the background for a bounded window.
| Setting | Scope | Default | Description |
|---|---|---|---|
timeout |
global | 30 |
Timeout for LLM API calls, in seconds |
drawTimeout |
global | 120 |
Timeout for image generation. 0 uses the global timeout |
askExpiry |
global | 60 |
Seconds to keep retrying a timed-out @ask. 0 disables |
codeExpiry |
global | 60 |
Seconds to keep retrying a timed-out @code. 0 disables |
drawExpiry |
global | 60 |
Seconds to keep retrying a timed-out @draw. 0 disables |
Draw behaviour¶
| Setting | Scope | Default | Description |
|---|---|---|---|
drawEnabled |
channel | True |
False refuses @draw and hides the generate_image tool from chat. The tool is also hidden when imageModel's provider has no key |
drawAutoRewriteMax |
channel | 1 |
Automatic prompt rewordings when a safety filter blocks a request. 0 disables. Each rewording costs a second billed image call on top of the refused one, and every recovery observed in prod landed on the first attempt, so raising this mostly buys repeat refusals of a prompt that was never going to pass |
drawContextMaxAgeSeconds |
channel | 60 |
Pass conversation context to draw requests only when the last activity is this recent. 0 always starts fresh |
Animate behaviour¶
@animate renders on a self-hosted video box (see Video generation for the credential). Generation is asynchronous: the command submits a job and returns, and the pending-task poller publishes the clip when it lands, so a bot restart mid-render does not lose the video. The box renders one clip at a time, so the queue is capped (animateMaxPendingPerUser, animateMaxPending) and @renders lets users and admins inspect or clear it.
| Setting | Scope | Default | Description |
|---|---|---|---|
animateEnabled |
channel | True |
False refuses @animate and hides the generate_video tool from chat. Both are off everywhere until the video box is configured |
animateSteps |
channel | 25 |
Denoising steps, and the dominant cost knob — latency is roughly linear in it. Measured on the reference box at 1280x704: a four-second clip took 68s at 25 steps and 171s at 50; at 25 steps a seven-second clip took 135s |
animateSize |
channel | 1280x704 |
Output resolution. Must be a geometry the loaded model supports |
animateDuration |
channel | 7 |
Clip length in seconds. The whole clip is exclusive GPU time, so raising it slows every queued request behind it. Also watch the file size: a 7s clip measured 8.87 MB, leaving about 1 MB under the uploader's 10 MB ceiling, and a clip over it falls back to local storage instead of imageUploadUrl |
animateAudio |
channel | True |
Generate a soundtrack with the video. False requests silent video |
animateModel |
channel | empty | Model to request. Empty sends no model field, letting a single-model box pick its own — usually right |
animateFlowShift |
global | 12 |
Flow-matching shift for the video sampler |
animateAudioFlowShift |
global | 3 |
Flow-matching shift for the audio track. Ignored when animateAudio is off |
animateTimeout |
global | 60 |
Timeout for individual HTTP calls to the video server. Not the generation budget — that is animateExpiry |
animateExpiry |
global | 1800 |
Seconds to keep polling a submitted job before reporting it expired. Generous on purpose: a queued job can wait a long time before it starts |
animateMaxPendingPerUser |
global | 2 |
How many clips one person may have waiting to render at once. A request past this is refused before anything is submitted. Owner and admin are exempt. 0 disables the per-user cap |
animateMaxPending |
global | 6 |
How many clips may be waiting to render across the whole bot. The box renders one at a time, so this bounds the longest wait anyone is promised. Applies to everyone. 0 disables |
animateRenderSeconds |
global | 135 |
Seconds one clip takes to render at the configured size, duration and steps. Only used to estimate the wait quoted when a clip is queued; measure it on the box after changing animateDuration or animateSteps |
Clips are published to imageUploadUrl when one is configured — the reference host accepts MP4 on the same images[] field and files it as vid_*.mp4 — and fall back to local storage otherwise, including when a clip exceeds the 10 MB upload ceiling.
Other settings¶
| Setting | Scope | Default | Description |
|---|---|---|---|
maxPromptLength |
global | 10000 |
Longest accepted user prompt, in characters |
commandPrefixes |
global | . @ |
Space-separated prefixes sanitised in output to prevent IRC command injection. The default covers both the conventional . and VibeBot's own @ |
databasePath |
global | empty | Path to the SQLite database. Empty uses data/LLM.db |
logLevel |
global | WARNING |
Plugin log level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
metaMaxSteps |
global | 12 |
Cap on tool-call round trips per assistant turn. Stops runaway tool loops |
maxConcurrentLLMCalls |
global | 16 |
Cap on simultaneous outbound LLM calls across all surfaces |
skipAutoWhoOnJoin |
global | True |
Skip Limnoria's automatic WHO on join when the account-tag and extended-join capabilities are active |
enforceRateLimits |
global | True |
Enforce rate limits. False tracks and logs without blocking |