How to Connect ChatGPT or Claude to a Telegram Bot
Practice4 minInnovialabs editorial team
In short
The bot's backend receives a message, loads that user's recent history, calls the LLM API with a key stored only on the server, streams the answer by editing one message, and enforces per-user limits and input checks.
How the setup works
The flow is simple: Telegram → your backend → LLM API → back to Telegram. The backend receives an update via webhook or polling, adds the user's message to their history, sends the history to the OpenAI or Anthropic API and returns the answer to the chat.
Everything that matters for quality, cost and safety happens in that middle layer: context storage, streaming, formatting, limits and moderation.
API keys stay on the backend
- Keep the key in an environment variable or a secrets manager, never in the repository or a Mini App frontend.
- Use separate keys for development and production, so a leaked test key does not expose the live account.
- Set spending limits and alerts in the provider's console.
- If a key leaks, revoke it and issue a new one immediately; do not wait to see whether it gets used.
Dialog context per user
LLM APIs are stateless: each request must include the conversation so far. You store it yourself.
- Keep messages in Redis or a database, keyed by
chat_id(in groups, by chat and user). - Trim the history: keep the last N messages or stay within a token budget. Older parts can be replaced with a short summary.
- Add a
/newcommand so users can reset the context when they change topics. - Tell users that conversations are stored and for how long, and delete old data on schedule.
Long histories are the main reason costs grow quietly: every new message re-sends everything before it.
Streaming by editing messages
Waiting for a long answer with no feedback feels broken. The usual approach: send a placeholder, then edit it as text arrives.
import time
from aiogram.exceptions import TelegramBadRequest
async def stream_reply(message, history):
sent = await message.answer("...")
text, last_edit = "", 0.0
async for chunk in llm_stream(history): # your wrapper over the SDK
text += chunk
if time.monotonic() - last_edit > 1.5:
await safe_edit(sent, text)
last_edit = time.monotonic()
await safe_edit(sent, text)
return text
async def safe_edit(sent, text):
try:
await sent.edit_text(text[:4096])
except TelegramBadRequest:
pass # e.g. "message is not modified"
Key rules:
- Edit every second or two, not on every token. Telegram rate-limits edits, and too many requests return a 429 error with a
retry_aftervalue. - A message holds up to 4096 characters. When the answer grows past that, finish the current message and continue in a new one.
- Stream as plain text and apply formatting only to the final version.
- Newer Bot API versions add native draft streaming in private chats; check the official docs for what your library supports.
Markdown formatting pitfalls
Models write standard Markdown: **bold**, # headings, tables. Telegram's MarkdownV2 is a different dialect: bold is *text*, and characters such as _ * [ ] ( ) ~ > # + - = | { } . ! must be escaped. One unescaped dot or unclosed asterisk and the API returns "can't parse entities".
A reliable approach:
- Convert the model's Markdown to Telegram HTML (
<b>,<i>,<code>,<pre>,<a>), escaping<,>and&in the text. - Turn headings into bold lines and tables into lists or a code block, since Telegram has no tables.
- If sending still fails, fall back to plain text instead of losing the answer.
- Ask the model in the system prompt to avoid tables and deep nesting.
Rate and cost limits
| Limit | Why |
|---|---|
| Requests per user per minute | Stops spam and scripts |
| Daily token budget per user | Caps the cost of one heavy user |
| Max output tokens per answer | Prevents runaway responses |
| Max input length | Blocks pasted novels |
| One active generation per user | Avoids parallel requests from double taps |
| Global monthly cap with alerts | Protects the whole budget |
Count usage from the usage field in API responses rather than estimating, and show users a clear message when they hit a limit.
Moderating user input
- Check length and obvious spam before calling the model.
- Run input through a moderation endpoint or a cheap classifier model if the bot is public.
- Treat prompt injection as a given: users will try to override the system prompt. Do not put secrets in it, and if the model can call tools, validate every action on the server.
- Log flagged messages, let admins block users, and check outputs too if the bot works in public groups.