Most people who want a private AI chatbot stop at a local chat window, because a chat window does not live in the group chat their friends already use. Point a bot at a model running on your own hardware and the conversations stay where they started, on a machine you control, with nothing sent to a hosted endpoint. The work is wiring, not modeling, and almost all of it is documented somewhere other than the bot library’s README.
TL;DR
- Two endpoints, different jobs. Ollama’s OpenAI-compatible base URL is
http://localhost:11434/v1/(Ollama OpenAI compatibility), butkeep_aliveis not one of the fields that layer supports. For a long-lived bot, call the nativePOST /api/chat, wherekeep_alivedefaults to5m(Ollama API reference). - A five-minute idle timer is the first thing that bites. Ollama’s FAQ states that “By default models are kept in memory for 5 minutes before being unloaded,” so an idle bot pays a cold model load on the next message (Ollama FAQ).
- Discord will hand your bot empty messages until you flip a switch. Discord’s documentation is direct about the requirement: privileged intents “require you to toggle the intent for your app in your app’s settings within the Developer Portal” (Discord Gateway), and the bot code has to ask for it too with
intents.message_content = True(discord.py intro). - One request at a time, by default.
OLLAMA_NUM_PARALLELis “the maximum number of parallel requests each model will process at the same time, default 1,” which is a queue, not a chatbot (Ollama FAQ). - Both platforms rate-limit you, in different units. Discord documents that “All bots can make up to 50 requests per second to our API” (Discord rate limits); Telegram caps groups at 20 messages per minute (Telegram Bot FAQ).
- Nothing survives a reboot without a service manager.
Restart=is off by default: “If set tono(the default), the service will not be restarted” (systemd.service).
Point the bot at a local model
Start with Ollama, because it binds locally out of the box: “Ollama binds 127.0.0.1 port 11434 by default” (Ollama FAQ). That default matters for security. A bot server and its model on the same box need no authentication between them, and nothing is exposed to your network until you deliberately change it.
There are two ways to talk to it. The OpenAI-compatible layer sits at http://localhost:11434/v1/ and exposes /v1/chat/completions, /v1/completions, /v1/models, /v1/embeddings and POST /v1/responses (Ollama OpenAI compatibility). Use it when you want the stock OpenAI SDK. The Authorization header can carry any value; the docs describe api_key='ollama' as “required but ignored” against a local server.
Use the native POST /api/chat instead when you want control over how long the model stays resident. Its documented fields are model, messages, stream and keep_alive, and the last of these “controls how long the model will stay loaded into memory following the request (default: 5m)” (Ollama API reference). Setting stream to false returns a single response object instead of a stream of objects, which is what a chat bot wants.
messages is also where a bot keeps its memory. The API reference describes the field as “the messages of the chat, this can be used to keep a chat memory,” with system, user, assistant and tool as the documented roles (Ollama API reference). A system message is the right place for the bot’s personality and its length limit, and the single most effective thing you can put in it is an instruction to be brief.
Budget the context window before the history does
The window is small by default. Ollama’s FAQ gives the default as 4096 tokens (Ollama FAQ), and the context-length documentation shows how the automatic default scales with hardware: “4k context” below 24 GiB of VRAM, “32k context” between 24 and 48 GiB, and “256k context” at 48 GiB or more (Ollama context length).
A bot accumulates history faster than a person does, because every turn appends. Resending the whole transcript on each call is the default instinct and it is the wrong one. Trim to a rolling window, keep the system message pinned at the front, and expect truncation to arrive silently if you do not. Raising the window is possible with OLLAMA_CONTEXT_LENGTH, but the same documentation warns that “Setting a larger context length will increase the amount of memory required to run a model.”
Concurrency is the quieter limit. With OLLAMA_NUM_PARALLEL at its default of 1, two people talking at once means the second request waits (Ollama FAQ). That is usually fine for a small server and always fine for a private one. It is not fine for a channel with a dozen regulars, and the fix is the environment variable, not a faster GPU.
Discord: the intent you have to switch on twice
A bot that connects fine and then ignores every message is almost always an intents problem, which is why the library documentation’s own example asks for message bodies explicitly: intents = discord.Intents.default(), then intents.message_content = True, then the intents passed to the client, carrying a comment that “This example requires the ‘message_content’ intent” (discord.py intro).
Setting the flag in code is only half of it. Discord classifies MESSAGE_CONTENT alongside GUILD_PRESENCES and GUILD_MEMBERS as privileged intents that “require you to toggle the intent for your app in your app’s settings within the Developer Portal” (Discord Gateway). Both sides have to agree before any text arrives. The library’s entry point takes the token directly, as in client.run('my token goes here') (discord.py intro); keep the real token in an environment variable and read it there instead.
Rate limits differ more than you would expect
Discord limits are bucket-based and split into two layers: limits apply “on a per-route basis and globally” (Discord rate limits). Responses carry X-RateLimit-Remaining, X-RateLimit-Reset-After and a X-RateLimit-Bucket identifier, and going over produces an HTTP 429 whose retry_after field is “The number of seconds to wait before submitting another request.” The global ceiling is generous for a hobby bot: “All bots can make up to 50 requests per second to our API.”
Telegram constrains output rather than input, which is the constraint people trip on. In a single chat, “avoid sending more than one message per second”. In a group, bots “are not be able to send more than 20 messages per minute”. For bulk notifications, “bots are not able to broadcast more than about 30 messages per second” (Telegram Bot FAQ). A bot that splits a long answer into several messages will hit the group limit in ordinary conversation, so send one reply per user message.
Receiving works the same way on both platforms with one rule worth memorizing: “You can either use long polling or Webhooks,” and “it’s not possible to get updates via long polling while an outgoing Webhook is set” (Telegram Bot FAQ). On the library side, get_updates() is “Used for getting updates using long polling,” with set_webhook() and delete_webhook() for the other mode (python-telegram-bot). Pick one, and make sure the webhook is removed before you debug a silent bot.
Keeping it running
A bot that dies when you close the terminal was never hosted. A systemd unit is the smallest thing that makes it real, and it needs three directives. ExecStart= takes the “Commands that are executed when this service is started.” Restart= “Configures whether the service shall be restarted when the service process exits, is killed, or a timeout is reached”, and it is off by default: “If set to no (the default), the service will not be restarted.” RestartSec= sets the pause between attempts, defaulting to 100ms (systemd.service).
Set Restart=always, and the manual is specific about what that buys: “the service will be restarted regardless of whether it exited cleanly or not, got terminated abnormally by a signal, or hit a timeout.” Raise RestartSec= to something like 5s so a crash loop cannot hammer the GPU. Keep the bot token out of the unit file and out of version control; the unit’s job is to start the program, not to store its credentials.
If you would rather serve the model from llama.cpp than Ollama, the equivalent knobs are documented on the same page: --host defaults to 127.0.0.1 and --port to 9931, --api-key takes “multiple keys … as a comma-separated list,” and --parallel sets the “number of server slots (default: -1, -1 = auto)” (llama.cpp server README). Point the bot at that port instead of 11434 and nothing else in this guide changes.
What this means
Hosting a bot locally is ordinary operations work with three sharp edges, and all three are documented by the platform rather than by the bot library. The idle timer is first: a five-minute default unload makes every quiet period expensive unless you set keep_alive. The context window is second, because a bot’s history grows without anyone deciding to grow it. The platform rules are third, and they are the least negotiable, since the privileged-intent toggle and the per-group message cap are enforced entirely on someone else’s servers.
None of this requires a large model. A small model that answers in a few seconds suits a chat bot better than a large one that makes everyone wait, and the conversation history that would blow past a small context window is the thing you should be trimming anyway.
The bottom line
Run the model locally and keep it on 127.0.0.1. Call POST /api/chat with a system message and an explicit keep_alive, because the OpenAI-compatible layer does not expose that field. On Discord, toggle MESSAGE_CONTENT in the Developer Portal and set intents.message_content = True in the same change. Assume one in-flight request until you have measured otherwise, send one message per reply on Telegram, and put the bot behind a systemd unit with Restart=always and a RestartSec long enough that a crash cannot pin the GPU. That is the whole difference between a demo and something that is simply there.
Sources
- Ollama API reference, `POST /api/chat`: github.com/ollama/ollama/blob/main/docs/api.md
- Ollama FAQ, environment variables and defaults: github.com/ollama/ollama/blob/main/docs/faq.mdx
- Ollama documentation, OpenAI compatibility: docs.ollama.com/api/openai-compatibility
- Ollama documentation, context length: github.com/ollama/ollama/blob/main/docs/context-length.mdx
- llama.cpp server README: github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md
- discord.py introduction: discordpy.readthedocs.io/en/stable/intro.html
- Discord Gateway documentation, privileged intents: docs.discord.com/developers/events/gateway
- Discord developer documentation, rate limits: docs.discord.com/developers/topics/rate-limits
- python-telegram-bot `Bot` API reference: docs.python-telegram-bot.org/en/stable/telegram.bot.html
- Telegram Bot FAQ: core.telegram.org/bots/faq
- systemd.service manual: freedesktop.org/software/systemd/man/latest/systemd.service.html