A Discord bot that watches game-announcement channels across several servers,
summarizes each new announcement with a local LLM via any OpenAI-compatible API
(defaults to LM Studio on localhost:1234), and delivers the result to your
DM. The same DM is also a normal LLM chat.
- Summaries preserve game name, version, release date and maintenance windows,
with times converted to your timezone (default
Europe/Paris). - Reads image attachments with the vision model and extracts text from PDF attachments, so announcements posted as screenshots or PDFs still get summarized; the source image is re-attached to the forwarded DM summary.
- Each reply carries a small-text performance footer:
TTFT | Prefill | Decode | Context(measured by streaming internally; only the completed reply is ever sent). - Chat in DMs, or in-server: a button spins up a private per-user channel that remembers the last 5 exchanges (10 messages).
- One inference at a time (serial queue) — no GPU thrash, order preserved.
/modelslists the server's models and shows the selected one;/models <name>tells the server which model to use (sent as themodelfield on every request).- Graceful handling of a down/slow LLM (offline, timeout, empty-response, unknown-model).
- Node.js 18+ (developed on 26).
- An OpenAI-compatible LLM server with at least one model available — local
LM Studio (default,
http://localhost:1234/v1; app → Developer → Start Server, orlms server start) or a cloud one like OpenRouter (https://openrouter.ai/api/v1). Works with any/v1/chat/completionsserver. - A Discord application/bot you can invite to the announcement servers.
npm install- Create an app at https://discord.com/developers/applications.
- Bot tab → Reset Token → copy into
DISCORD_TOKEN. - Bot tab → enable the MESSAGE CONTENT INTENT (privileged — required to read announcements).
- General Information → copy the Application ID into
DISCORD_CLIENT_ID. - OAuth2 → URL Generator → scopes
bot+applications.commands; bot permissions: View Channels, Send Messages, Read Message History, and Manage Channels (the last only needed for server chat). Invite the bot to each server. - In Discord, enable Settings → Advanced → Developer Mode, then right-click each announcement channel → Copy ID, and right-click each recipient → Copy ID. (The bot can only DM a user who shares a server with it and allows DMs.)
cp .env.example .env # fill DISCORD_TOKEN, DISCORD_CLIENT_ID
cp config.example.json config.json # fill recipients + channel→game mapconfig.json:
| Field | Meaning |
|---|---|
summaryRecipients |
User IDs (array) that receive announcement summaries. |
chatUsers |
User IDs (array) allowed to chat with the bot in DMs — independent of summaries. |
announcementChannels |
Map of channelId → game name (any number of channels). |
serverChat.createChannelId |
Channel ID hosting the "Start a chat" button. Empty = server chat off. |
serverChat.categoryName |
Category the bot creates/uses for per-user chat channels (default Bot Chats). |
llm.model |
Model id the server should use (see /models). Empty = first available. |
llm.allowedModels / llm.excludedModels |
OpenRouter only: when llm.model is openrouter/auto, restrict which models the auto-router may pick (arrays; wildcards like anthropic/* allowed). |
llm.temperature, llm.maxTokens |
Generation settings. |
llm.timeoutSeconds |
Per-generation timeout (also covers model load on first use). |
timezone |
IANA zone for time conversion (default Europe/Paris). |
summaryRecipients and chatUsers are independent — a user can receive summaries
without chat access, or have chat access without receiving summaries. An older
single-user ownerUserId still works (it applies to both lists).
Set OPENAI_BASE_URL in .env only if your server isn't at the default
http://localhost:1234/v1 (e.g. http://192.168.1.10:1234/v1). Set OPENAI_API_KEY
only if the server requires one (LM Studio doesn't).
Using OpenRouter: set OPENAI_BASE_URL=https://openrouter.ai/api/v1, OPENAI_API_KEY
to your OpenRouter key, and llm.model to a model id like openai/gpt-4o or
anthropic/claude-3.5-sonnet (with 400+ models, don't leave it blank — the "first
available" default is arbitrary). Optionally set OPENROUTER_APP_URL / OPENROUTER_APP_NAME
for attribution. /models still lists and switches models, capped for readability.
To let OpenRouter auto-pick among a shortlist, set llm.model to openrouter/auto and
put the candidates in llm.allowedModels (e.g. ["anthropic/*", "openai/gpt-4o"]) — make
sure they're models your account can actually use, or the auto-router returns a 404.
npm run registerGlobal commands can take up to an hour to appear the first time. You don't have to
wait — typing /models (or /models <name>) directly in the DM works immediately.
npm start- A message in a mapped announcement channel → summarized → DM'd to every
summaryRecipientsuser. - A DM from a
chatUsersuser → answered as chat (each reply goes only to its sender; other users' DMs are ignored). - Server chat: in the create-channel, anyone clicks Start a chat → the bot makes a
private channel (that user + the bot only) under the
Bot Chatscategory. Messages there are answered with memory of the last 5 exchanges; a fresh channel is made on each click. - Everything else (other channels, bot messages, edits) → ignored.
- Time conversion is done by the model (per the system prompt); there's no separate date parser.
/models <name>matches by exact id or substring (e.g.gemma→google/gemma-4-e2b).- Attachments: images are read by the vision model (and re-attached to the DM
summary); PDFs have their text layer extracted (scanned/image-only PDFs yield no text
— no image-OCR fallback). Vision input plus reasoning can eat a lot of tokens, so if
image summaries come back empty, raise
llm.maxTokens. Up to 5 images per message, ≤20 MB each; other file types become a name+URL note.