Convert LLM Markdown, LaTeX formulas, and tables into native Telegram Bot API 10.1+ Rich HTML (sendRichMessage). Zero dependencies.
Python
0
8 commits
updated Sep 20, 2026
A lightweight, zero-dependency Python library that converts standard LLM Markdown, LaTeX formulas, thinking processes, and tables into native Telegram Bot API 10.1+ Rich HTML (sendRichMessage).
Starting with Telegram Bot API 10.1, Telegram introduced Rich Messages (sendRichMessage) supporting:
DeepSeek-R1, OpenAI o1/o3, Qwen, Gemini).<ul>, <ol>, <u>, <mark>).However, LLMs (OpenAI, Anthropic, DeepSeek, Ollama) still output plain Markdown and LaTeX. tg-rich-converter bridges this gap seamlessly in a single function call or via the CLI command line tool.
streaming=True): Auto-balances unclosed code blocks (```), reasoning tags (<think>), unclosed LaTeX formulas ($$ / $), and unclosed inline styles (**, ||, ++, ==, ~~) during token-by-token streaming, completely preventing Telegram 400 Bad Request: can't parse entities errors on live message edits.<table bordered striped> with column alignment (left, center, right). Safely handles formulas with pipes ($|\psi\rangle$, $|x| \ge 0$) and escaped pipes (\|) inside table cells without breaking columns.$$...$$ into <tg-math-block> and $x$ into <tg-math>.<think>...</think> tags from reasoning models into expandable <details><summary>Размышления</summary>...</details> blocks with configurable summary titles.<, >, and & in regular text (e.g. mathematical conditions like x < 5 and y > 10), completely preventing Telegram API 400 Bad Request: can't parse entities errors.- , * , + ) and ordered (1. ) lists into native <ul> and <ol> tags, avoiding conflicts between asterisk bullet markers and italics.**), italic (* / _), strikethrough (~~), underline (++text++), highlight (==text==), and spoilers (||spoiler||) with snake_case protection.<pre><code class="language-..."> preserving language classes, indentation, and copy buttons.<pre><code class="...">, <blockquote>), and protects LaTeX formulas from fragmentation.preview.html styled with authentic Telegram Web dark theme and KaTeX client-side math rendering to visually inspect output without launching a bot.tg-rich): Fast command-line utility with TrueColor ANSI logo, preview generator, auto-opening in browser, pipeline support (stdin/stdout), and batch message splitter.re, html, argparse). Fully reentrant and async-safe for high-concurrency bot environments.pip install tg-rich-converter
from tg_rich_converter import to_rich
llm_output = """
# Quantum Computing Report
| Algorithm | State | Complexity | Option |
|:----------|:-----:|:----------:|-------:|
| Linear Search | $N$ items | $O(N)$ | Mode A \\| B |
| State Vector | $|\\psi\\rangle$ | $O(1)$ | Basic |
### Key Formula
$$|\\psi\\rangle = \\alpha |0\\rangle + \\beta |1\\rangle$$
Stability requires delta < 0.05 and alpha > 0.
<think>
Evaluating time complexity and qubit entanglement...
</think>
"""
# Default thinking summary is "Размышления"
rich_html = to_rich(llm_output)
# Or specify a custom summary:
rich_html_en = to_rich(llm_output, thinking_summary="Reasoning Process")
When streaming LLM responses token-by-token (OpenAI, DeepSeek-R1, Anthropic Claude, Ollama), intermediate tokens frequently contain unfinished Markdown/LaTeX structures:
python\ndef run():`` (missing closing )<think>Analyzing steps... (missing </think>)$E = mc^2 or $$\int_0^\infty**bold text, ||secret key, ++underlinedPassing streaming=True automatically balances and virtually closes all open structures in LIFO order for every frame:
import time
from tg_rich_converter import to_rich
# 1. Send initial placeholder message via sendRichMessage
sent_msg = await bot.send_rich_message(
chat_id=chat_id,
rich_message={"html": "<i>⏳ Thinking...</i>"}
)
buffer = ""
last_edit_time = time.time()
THROTTLE_SECONDS = 0.7 # Recommended: 0.6 - 0.8s to avoid Telegram 429 Flood Limits
# 2. Stream tokens from LLM
async for chunk in openai_client.chat.completions.create(..., stream=True):
buffer += chunk.choices[0].delta.content or ""
now = time.time()
# Edit message with rate-limiting throttle
if now - last_edit_time >= THROTTLE_SECONDS:
safe_frame_html = to_rich(buffer, streaming=True)
await bot.edit_message_text(
chat_id=chat_id,
message_id=sent_msg.message_id,
rich_message={"html": safe_frame_html} # Must pass rich_message, NOT text/parse_mode!
)
last_edit_time = now
# 3. Final complete render (streaming=False)
final_html = to_rich(buffer, streaming=False)
await bot.edit_message_text(
chat_id=chat_id,
message_id=sent_msg.message_id,
rich_message={"html": final_html}
)
Bad Request: can't parse entities: Unsupported start tag "details"sendMessage or editMessageText with legacy text="<details>..." and parse_mode="HTML". The legacy Telegram parser does not support <details>, <tg-math>, or <table>.rich_message={"html": ...}:
# ❌ WRONG (Triggers 400 Bad Request):
await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=rich_html, parse_mode="HTML")
# ✔ CORRECT (Native Rich Messages):
await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, rich_message={"html": rich_html})
Too Many Requests: retry after X (Flood Limits)editMessageText on every single incoming LLM token.<think> reasoning block headerthinking_summary:
html_output = to_rich(markdown_text, thinking_summary="Chain of Thought")
After installation, the tg-rich command is available in your terminal:
Convert a markdown file and open the interactive Telegram preview directly in your default browser:
tg-rich prompt_response.md --preview --open
tg-rich document.md -o output.html
cat llm_output.md | tg-rich > telegram_message.html
Split a large document into chunks respecting Telegram character limits:
# Split for Rich Messages (32k limit)
tg-rich big_report.md --split
# Split for Classic Messages (4k limit) into separate files
tg-rich big_report.md --split --limit 4096 -o chunk.html
| Flag | Description | Default |
|---|---|---|
input_file | Path to markdown file (or - / pipe for stdin) | - |
-o, --output FILE | Write converted HTML to file instead of stdout | stdout |
-p, --preview [FILE] | Generate standalone preview.html with KaTeX & Telegram Dark theme | preview.html |
--open | Automatically open the preview in default web browser | False |
-s, --split | Split long message into safe Telegram chunks | False |
-l, --limit INT | Maximum character length for splitting | 32768 |
-t, --thinking-summary TEXT | Custom header for <think> reasoning blocks | Размышления |
--lang {ru,en} | Interface and error message language | auto |
-q, --quiet | Suppress banner and progress messages in stderr | False |
-v, --version | Display current library version |
When LLM output exceeds Telegram limits (32,768 chars for Rich Messages or 4,096 chars for standard messages), naive slicing breaks open HTML tags and crashes the bot. Use split_rich_message:
from tg_rich_converter import split_rich_message
# Automatically converts Markdown to Rich HTML and splits into safe chunks
chunks = split_rich_message(
long_llm_response,
max_length=32768, # 32,768 for Rich Messages (default) or 4,096 for Classic
is_markdown=True, # Automatically runs to_rich()
thinking_summary="Reasoning"
)
# Each chunk is guaranteed to be valid HTML with all open tags closed and reopened
for chunk in chunks:
await bot.send_rich_message(chat_id=chat_id, rich_message={"html": chunk})
Visualize how your message will look in Telegram Desktop/Mobile without running a bot or sending messages:
from tg_rich_converter import save_preview
# Generates preview.html with Telegram dark theme, KaTeX formulas, and interactive spoilers
save_preview(
llm_output,
file_path="preview.html",
title="LLM Telegram Preview"
)
Double click preview.html to open it in your browser!
from aiogram import Bot
from tg_rich_converter import to_rich
bot = Bot(token="YOUR_BOT_TOKEN")
rich_html = to_rich(llm_response)
await bot.send_rich_message(
chat_id=chat_id,
rich_message={"html": rich_html}
)
import telebot
from tg_rich_converter import to_rich
bot = telebot.TeleBot("YOUR_BOT_TOKEN")
rich_html = to_rich(llm_response)
bot.send_rich_message(
chat_id=chat_id,
rich_message={"html": rich_html}
)
import requests
from tg_rich_converter import to_rich
rich_html = to_rich(llm_response)
requests.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendRichMessage",
json={
"chat_id": chat_id,
"rich_message": {
"html": rich_html
}
}
)
Run the comprehensive test suite locally (44 tests, 100% pass):
pytest
Test real-time LLM token-by-token streaming with rate-limiting throttle (0.7s) directly in your Telegram chat:
# Direct CLI arguments
python demo_streaming.py --token "YOUR_BOT_TOKEN" --chat-id "YOUR_CHAT_ID"
# Or via environment variables
export BOT_TOKEN="YOUR_BOT_TOKEN"
export CHAT_ID="YOUR_CHAT_ID"
python demo_streaming.py
# Windows PowerShell
$env:BOT_TOKEN="YOUR_BOT_TOKEN"; $env:CHAT_ID="YOUR_CHAT_ID"; python demo_streaming.py
Validate intermediate streaming frames in terminal without sending requests to Telegram:
python demo_streaming.py
MIT License. Free for commercial and personal use.
8 commits
Python
100.0%
Convert LLM Markdown, LaTeX formulas, and tables into native Telegram Bot API 10.1+ Rich HTML (sendRichMessage). Zero dependencies.
Python
0
8 commits
updated Sep 20, 2026
A lightweight, zero-dependency Python library that converts standard LLM Markdown, LaTeX formulas, thinking processes, and tables into native Telegram Bot API 10.1+ Rich HTML (sendRichMessage).
Starting with Telegram Bot API 10.1, Telegram introduced Rich Messages (sendRichMessage) supporting:
DeepSeek-R1, OpenAI o1/o3, Qwen, Gemini).<ul>, <ol>, <u>, <mark>).However, LLMs (OpenAI, Anthropic, DeepSeek, Ollama) still output plain Markdown and LaTeX. tg-rich-converter bridges this gap seamlessly in a single function call or via the CLI command line tool.
streaming=True): Auto-balances unclosed code blocks (```), reasoning tags (<think>), unclosed LaTeX formulas ($$ / $), and unclosed inline styles (**, ||, ++, ==, ~~) during token-by-token streaming, completely preventing Telegram 400 Bad Request: can't parse entities errors on live message edits.<table bordered striped> with column alignment (left, center, right). Safely handles formulas with pipes ($|\psi\rangle$, $|x| \ge 0$) and escaped pipes (\|) inside table cells without breaking columns.$$...$$ into <tg-math-block> and $x$ into <tg-math>.<think>...</think> tags from reasoning models into expandable <details><summary>Размышления</summary>...</details> blocks with configurable summary titles.<, >, and & in regular text (e.g. mathematical conditions like x < 5 and y > 10), completely preventing Telegram API 400 Bad Request: can't parse entities errors.- , * , + ) and ordered (1. ) lists into native <ul> and <ol> tags, avoiding conflicts between asterisk bullet markers and italics.**), italic (* / _), strikethrough (~~), underline (++text++), highlight (==text==), and spoilers (||spoiler||) with snake_case protection.<pre><code class="language-..."> preserving language classes, indentation, and copy buttons.<pre><code class="...">, <blockquote>), and protects LaTeX formulas from fragmentation.preview.html styled with authentic Telegram Web dark theme and KaTeX client-side math rendering to visually inspect output without launching a bot.tg-rich): Fast command-line utility with TrueColor ANSI logo, preview generator, auto-opening in browser, pipeline support (stdin/stdout), and batch message splitter.re, html, argparse). Fully reentrant and async-safe for high-concurrency bot environments.pip install tg-rich-converter
from tg_rich_converter import to_rich
llm_output = """
# Quantum Computing Report
| Algorithm | State | Complexity | Option |
|:----------|:-----:|:----------:|-------:|
| Linear Search | $N$ items | $O(N)$ | Mode A \\| B |
| State Vector | $|\\psi\\rangle$ | $O(1)$ | Basic |
### Key Formula
$$|\\psi\\rangle = \\alpha |0\\rangle + \\beta |1\\rangle$$
Stability requires delta < 0.05 and alpha > 0.
<think>
Evaluating time complexity and qubit entanglement...
</think>
"""
# Default thinking summary is "Размышления"
rich_html = to_rich(llm_output)
# Or specify a custom summary:
rich_html_en = to_rich(llm_output, thinking_summary="Reasoning Process")
When streaming LLM responses token-by-token (OpenAI, DeepSeek-R1, Anthropic Claude, Ollama), intermediate tokens frequently contain unfinished Markdown/LaTeX structures:
python\ndef run():`` (missing closing )<think>Analyzing steps... (missing </think>)$E = mc^2 or $$\int_0^\infty**bold text, ||secret key, ++underlinedPassing streaming=True automatically balances and virtually closes all open structures in LIFO order for every frame:
import time
from tg_rich_converter import to_rich
# 1. Send initial placeholder message via sendRichMessage
sent_msg = await bot.send_rich_message(
chat_id=chat_id,
rich_message={"html": "<i>⏳ Thinking...</i>"}
)
buffer = ""
last_edit_time = time.time()
THROTTLE_SECONDS = 0.7 # Recommended: 0.6 - 0.8s to avoid Telegram 429 Flood Limits
# 2. Stream tokens from LLM
async for chunk in openai_client.chat.completions.create(..., stream=True):
buffer += chunk.choices[0].delta.content or ""
now = time.time()
# Edit message with rate-limiting throttle
if now - last_edit_time >= THROTTLE_SECONDS:
safe_frame_html = to_rich(buffer, streaming=True)
await bot.edit_message_text(
chat_id=chat_id,
message_id=sent_msg.message_id,
rich_message={"html": safe_frame_html} # Must pass rich_message, NOT text/parse_mode!
)
last_edit_time = now
# 3. Final complete render (streaming=False)
final_html = to_rich(buffer, streaming=False)
await bot.edit_message_text(
chat_id=chat_id,
message_id=sent_msg.message_id,
rich_message={"html": final_html}
)
Bad Request: can't parse entities: Unsupported start tag "details"sendMessage or editMessageText with legacy text="<details>..." and parse_mode="HTML". The legacy Telegram parser does not support <details>, <tg-math>, or <table>.rich_message={"html": ...}:
# ❌ WRONG (Triggers 400 Bad Request):
await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=rich_html, parse_mode="HTML")
# ✔ CORRECT (Native Rich Messages):
await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, rich_message={"html": rich_html})
Too Many Requests: retry after X (Flood Limits)editMessageText on every single incoming LLM token.<think> reasoning block headerthinking_summary:
html_output = to_rich(markdown_text, thinking_summary="Chain of Thought")
After installation, the tg-rich command is available in your terminal:
Convert a markdown file and open the interactive Telegram preview directly in your default browser:
tg-rich prompt_response.md --preview --open
tg-rich document.md -o output.html
cat llm_output.md | tg-rich > telegram_message.html
Split a large document into chunks respecting Telegram character limits:
# Split for Rich Messages (32k limit)
tg-rich big_report.md --split
# Split for Classic Messages (4k limit) into separate files
tg-rich big_report.md --split --limit 4096 -o chunk.html
| Flag | Description | Default |
|---|---|---|
input_file | Path to markdown file (or - / pipe for stdin) | - |
-o, --output FILE | Write converted HTML to file instead of stdout | stdout |
-p, --preview [FILE] | Generate standalone preview.html with KaTeX & Telegram Dark theme | preview.html |
--open | Automatically open the preview in default web browser | False |
-s, --split | Split long message into safe Telegram chunks | False |
-l, --limit INT | Maximum character length for splitting | 32768 |
-t, --thinking-summary TEXT | Custom header for <think> reasoning blocks | Размышления |
--lang {ru,en} | Interface and error message language | auto |
-q, --quiet | Suppress banner and progress messages in stderr | False |
-v, --version | Display current library version |
When LLM output exceeds Telegram limits (32,768 chars for Rich Messages or 4,096 chars for standard messages), naive slicing breaks open HTML tags and crashes the bot. Use split_rich_message:
from tg_rich_converter import split_rich_message
# Automatically converts Markdown to Rich HTML and splits into safe chunks
chunks = split_rich_message(
long_llm_response,
max_length=32768, # 32,768 for Rich Messages (default) or 4,096 for Classic
is_markdown=True, # Automatically runs to_rich()
thinking_summary="Reasoning"
)
# Each chunk is guaranteed to be valid HTML with all open tags closed and reopened
for chunk in chunks:
await bot.send_rich_message(chat_id=chat_id, rich_message={"html": chunk})
Visualize how your message will look in Telegram Desktop/Mobile without running a bot or sending messages:
from tg_rich_converter import save_preview
# Generates preview.html with Telegram dark theme, KaTeX formulas, and interactive spoilers
save_preview(
llm_output,
file_path="preview.html",
title="LLM Telegram Preview"
)
Double click preview.html to open it in your browser!
from aiogram import Bot
from tg_rich_converter import to_rich
bot = Bot(token="YOUR_BOT_TOKEN")
rich_html = to_rich(llm_response)
await bot.send_rich_message(
chat_id=chat_id,
rich_message={"html": rich_html}
)
import telebot
from tg_rich_converter import to_rich
bot = telebot.TeleBot("YOUR_BOT_TOKEN")
rich_html = to_rich(llm_response)
bot.send_rich_message(
chat_id=chat_id,
rich_message={"html": rich_html}
)
import requests
from tg_rich_converter import to_rich
rich_html = to_rich(llm_response)
requests.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendRichMessage",
json={
"chat_id": chat_id,
"rich_message": {
"html": rich_html
}
}
)
Run the comprehensive test suite locally (44 tests, 100% pass):
pytest
Test real-time LLM token-by-token streaming with rate-limiting throttle (0.7s) directly in your Telegram chat:
# Direct CLI arguments
python demo_streaming.py --token "YOUR_BOT_TOKEN" --chat-id "YOUR_CHAT_ID"
# Or via environment variables
export BOT_TOKEN="YOUR_BOT_TOKEN"
export CHAT_ID="YOUR_CHAT_ID"
python demo_streaming.py
# Windows PowerShell
$env:BOT_TOKEN="YOUR_BOT_TOKEN"; $env:CHAT_ID="YOUR_CHAT_ID"; python demo_streaming.py
Validate intermediate streaming frames in terminal without sending requests to Telegram:
python demo_streaming.py
MIT License. Free for commercial and personal use.
8 commits
Python
100.0%