--- title: Lumen colorFrom: yellow colorTo: gray sdk: docker app_port: 7860 pinned: false ---

Lumen: Intelligence. Clarity.

CI Python 3.13 License: MIT

A Telegram bot styled after Claude's tone and personality (direct, warm, light on hedging), running on Munito AI with Google Gemini as the automatic reserve and automatic per-message routing between them. Groq is left for one job: transcribing voice notes. Lumen also generates images, downloads TikTok videos (usually without watermarks, watermarked copy as a last resort), and reads text back as speech. It runs as a webhook service on Hugging Face Spaces (Docker). **Stack:** Python 3.13 · aiogram · FastAPI · google-genai · Docker **Русская версия: [README.ru.md](README.ru.md).** ## Contents - [Features](#features) - [How it works](#how-it-works) - [Deploying on Hugging Face Spaces](#deploying-on-hugging-face-spaces) - [Configuration](#configuration) - [Proxy setup](#proxy-setup) - [Diagnostics](#diagnostics) - [Commands](#commands) - [Testing](#testing) - [Known limitations](#known-limitations) - [Security](#security) - [License](#license) > The YAML block above is Hugging Face Space metadata, not a mistake: this same `README.md` doubles as the Space card. Everything below it is normal GitHub documentation. ## Features - **Automatic model routing.** Every message goes to whichever provider actually has what it needs: plain text and images start at Munito AI (open models over one OpenAI-compatible endpoint), with Gemini as the automatic reserve; link reading, video and audio go to Gemini, since no other provider can do them — and grounded search with the provider's own citations is Gemini-only too. Voice notes are transcribed by Groq Whisper first, which is the only thing Groq is left for. Every fresh query also gets real search results for whichever model answers, and since 12.11.2026 the model writes the queries itself: the Munito head is handed a `web_search` tool, asks the web in its own words, sees the results and may refine the query, and everything collected is attached for whatever model finally answers (exact sources — encyclopaedia, crypto and FX prices, weather — dated news and the general web are queried in parallel). How deeply a message is thought about is chosen per request the way Claude's effort works, and the same choice bounds how many searches that message may run. - **Streaming replies** with a self-calibrating typing-speed pacer, so answers type themselves in instead of landing in a few large chunks. - **Claude-style live status.** In private and group chats search and media requests show a sparkle timeline ("Searching the web: ", found sources, a thinking timer) and finish with a collapsed "Thought for N seconds" block; simple answers get a subtle italic thought line. Private answers are plain messages, without Telegram reply quotes. - **Defenses against prompt injection and identity leaks.** A deterministic input filter plus output scrubbers keep the bot from revealing which model or provider actually answered. - **TikTok downloads** (usually without watermarks; a watermarked copy is the last resort): video, slideshows (including "live" photo slides), and original sound. - **Image generation** through Cloudflare Workers AI (FLUX.2-klein-4b), with Pollinations.ai as the fallback. Both are free tiers; every request carries a random seed and the safety filter on. The request is rewritten into a finished frame description before it leaves, so a short phrase is enough. Edits work on an attached or replied photo (two at once for «совмести это с этим»), «ещё вариант» and «сделай 3 варианта» give a series, the aspect ratio comes from plain words (16:9, «vertical», «poster», «phone wallpaper»), and «верни предыдущий вариант» undoes to the earlier picture without spending another slot. - **Text-to-speech** through Gemini TTS. - `/draw` and `/tts` also work as plain phrases at the start of a message ("draw a cat," "read this out loud"), no slash required. - **Clarifying buttons** for short taste requests without details ("посоветуй фильм" → genre buttons instead of a guess). - **Captionless media** (a photo, video or voice note sent without text) gets a description/summary in the chat's language; with no `/lang` set, Telegram's client language is used, and unsupported codes fall back to the model's own choice. - **Bot language** (`/lang`): system messages in 25 languages (English by default). AI answers always follow the user's own language. The Telegram profile too — bot name, the "About" one-liner and the long description — is registered from `lumen_lang.py` at startup in 24 locales (`PROFILE_LANGS`), so it is edited in code, not in BotFather. - **Persistent state.** An optional Upstash Redis backend keeps chat history and quota counters alive across redeploys. - **Error tracking** through an optional Sentry integration that scrubs secrets before sending anything. ## How it works Lumen is a single FastAPI + aiogram service. Telegram delivers updates to a webhook; duplicate `update_id` deliveries are ignored. Each message gets routed through a chain of candidate models, built on the fly from the message's content: attachments, links, and a couple of lightweight heuristics for "does this need current information" and "is this a heavy request." Plain text and images start on Munito AI, with Gemini as the automatic reserve; links, YouTube and video/audio go to Gemini, the only provider with native grounded search; since 12.11.2026 the first Munito head of a text route writes the web-search queries itself through a `web_search` tool. Every fresh query still gets an external search block attached for any provider (fact APIs, dated news, general web). The first provider that answers wins, the next one is tried as a fallback if its whole chain fails. Models that already returned 429 today are skipped by the router. The codebase is a modular monolith. `bot.py` is the entry point and composition root; the rest is split into focused modules: `lumen_admin.py` (FastAPI app, secret gates, admin/diag/export endpoints), `lumen_message_core.py` (message-handling core), `lumen_routes.py` (LLM calls with fallback, `_run_route`), `lumen_streaming.py` (provider-agnostic streaming), `lumen_status.py` (Claude-style live status/activity bubble), `lumen_search.py` (external search: query planning, fact/news/web layers, optional keyed provider), `lumen_agent.py` (the model-driven `web_search` loop: tool schema, planning calls, source gathering, step lines), `lumen_chat_state.py` (chat state, quotas, locks), `lumen_commands.py` (commands, TTS/draw pipelines, pick buttons), `lumen_tiktok_flow.py` (TikTok send orchestration), `lumen_rich.py` (sending: guests, Rich Messages, chunking), `lumen_media_flow.py` (Telegram attachment downloads), `lumen_transport_calls.py` (sessions, proxy rotation, Telegram calls), `lumen_errors.py` (model error classification), `lumen_limits.py` (rate window, pick queue), `lumen_router_config.py` (model routing), `lumen_formatting.py` (markdown to Telegram HTML, plus Rich Messages for tables/headings/math), `lumen_security.py` (injection and leak defenses), `lumen_message_parse.py` (links, draw/tts triggers, media references), `lumen_media.py` (mime types, file-id parsing), `lumen_images.py`, `lumen_tts.py`, `lumen_tiktok.py`, `lumen_telegram_transport.py`, `lumen_state_storage.py`, `lumen_typing_pace.py`, `lumen_model_speed.py`, and `lumen_lang.py` (bot language: system messages in 25 languages). `lumen_devtrace.py` writes an opt-in JSONL trace (requests, answers, passive background) for prompt debugging. See **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** for a deeper look at routing, streaming, the security layers, and the TikTok downloader. ## Deploying on Hugging Face Spaces 1. Push this repository to a Space (`sdk: docker` is already set in the frontmatter above). 2. Set the required secrets: `BOT_TOKEN`, `GEMINI_API_KEY`, and `ADMIN_SECRET_SEED`. Production refuses to start without the seed: admin keys must not be derived from the bot token. 3. Hugging Face builds the image from `Dockerfile` and runs `python -u bot.py`. 4. On startup the bot registers its own webhook. If that fails, check the logs for `[webhook] setWebhook failed` and register it manually: ```bash curl -H "Authorization: Bearer " https:///webhook_url ``` then fill the `register_url_template` it returns (token by hand, via curl — never open a token URL in a browser). 5. `WEBHOOK_SECRET` and `ADMIN_PANEL_KEY` are derived deterministically from `ADMIN_SECRET_SEED`. Fetch them with: ```bash curl -H "Authorization: Bearer " https:///admin_keys ``` Hugging Face Spaces' outbound IPs are blocked by Telegram's API at the network level, so a small proxy is required. See [Proxy setup](#proxy-setup) below. ## Configuration Locally only `BOT_TOKEN` and `GEMINI_API_KEY` are required; on the HF Space `ADMIN_SECRET_SEED` is required as well. Everything else has a sane default; the full reference (proxy addresses, timeouts, storage, rate limiting, TikTok tuning, media concurrency) lives in **[docs/ENVIRONMENT.md](docs/ENVIRONMENT.md)**. ## Proxy setup Hugging Face Spaces' outbound IPs are blocked by Telegram's Bot API entirely, and TikWM (the TikTok API this bot uses) returns `403` to the same IPs. Both are fixed by one small pass-through proxy, deployed separately on Deno Deploy (source: `proxy/proxy.ts`): ```bash TELEGRAM_API_BASE_URL=https:///fetch/api.telegram.org TIKWM_API_BASE_URL=https:///fetch/www.tikwm.com CLOUDFLARE_AI_BASE_URL=https:///fetch/api.cloudflare.com/client/v4/accounts LUMEN_PROXY_SECRET=<случайная строка от 32 символов: без неё прокси отвечает 503, а бот падает на старте> ``` The proxy only forwards to an explicit host allowlist (`api.telegram.org`, `www.tikwm.com`, `tikwm.com`, and `api.cloudflare.com` — the latter restricted to `/client/v4/accounts//ai/...`, because the image API token rides along in `Authorization`). `api.cloudflare.com` is here for the same reason as TikWM: Hugging Face egress to it silently hangs (prod 09.11.2026). See `proxy/proxy.ts` for the implementation and `proxy/proxy_test.ts` for its tests; a green CI run on `main` ships it through `.github/workflows/deploy-proxy.yml`, but that deploy is conditional on the tested commit's diff touching `proxy/` and has missed a change once (prod 09.11.2026): after editing the allowlist, confirm the deploy steps actually ran or ship it by hand (see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#deploy-paths)). ## Diagnostics All endpoints below require `Authorization: Bearer ` (`/admin_keys`, covered in the deploy steps above, uses `BOT_TOKEN` instead): | Endpoint | Purpose | |---|---| | `GET /diag` | Checks outbound network reachability (Telegram, Gemini, Groq, Munito, TikWM, Cloudflare AI, Pollinations, etc.) from inside the container. | | `GET /webhook_url` | Shows the computed webhook URL and a ready-to-open registration link. | | `GET /export_state` | Dumps all chat histories and quota counters as JSON, for manual backup. | | `GET /devtrace?n=200` | Tails the dev trace (requests, answers, passive background) as JSON, for prompt debugging. Needs `DEV_TRACE=1`. | `/logs` (a Telegram command, owner-only) sends the current `bot.log` with secrets redacted. ## Commands `/start`, `/reset`, `/draw`, `/tts`, and `/lang` show up in Telegram's command menu: | Command | Access | Purpose | |---|---|---| | `/start` (`/help` is the same command, kept out of the menu) | anyone | Shows a short intro and the command list. | | `/reset` | anyone in DMs; group admins/owner in groups | Clears the chat's conversation history. | | `/draw [description]` | anyone | Generates an image (see [Features](#features)). | | `/tts [text]` | anyone | Reads text out loud. | | `/lang` | anyone in DMs; group admins/owner in groups | Bot language menu (system messages; AI answers always follow your language). | More owner-only commands stay out of the menu on purpose, since they surface internal details that shouldn't be visible in a group chat: | Command | Access | Purpose | |---|---|---| | `/stats` | bot owner, DMs only | Active chat count, process uptime, per-model quota usage. | | `/logs` | bot owner, DMs only | Sends `bot.log` with secrets redacted. | | `/selftest [gemini]` | bot owner, DMs only, max once a minute | Live probe of the route heads (Munito, then Gemini only with the `gemini` argument) plus the Telegram proxy and TikWM. Successful probes count against quota. | | `/ban ` / `/unban ` (or as a reply) | bot owner, DMs only | Block/unblock a user by Telegram ID. Banned users are fully ignored: no answers, no group context, no limit counters. Survives restarts. | | `/banlist` | bot owner, DMs only | Shows banned user IDs. | The bot keeps one chronological chat memory of up to 200 messages (group background, messages addressed to it and its answers, with author and time); older messages are folded into a long-term summary. Photos are kept as Telegram references, and up to two recent photos are attached as context. `/reset` (group admins/owner) clears the memory and its summary. ## Testing ```bash pip install -r requirements.txt -r requirements-dev.txt pytest -v ``` Test files live in `tests/` and mirror the module split: `test_lumen_formatting.py`, `test_lumen_security.py`, `test_lumen_router_config.py`, `test_lumen_typing_pace.py`, `test_lumen_images.py`, `test_lumen_media.py`, `test_lumen_model_speed.py`, `test_lumen_search.py`, `test_lumen_status.py`, and `test_lumen_devtrace.py` each test their matching module directly, while `test_bot_admin.py`, `test_bot_commands.py`, `test_bot_routes.py`, `test_bot_search.py`, `test_bot_state.py`, `test_bot_streaming.py`, `test_bot_tiktok.py`, `test_bot_transport.py`, and `test_bot_tts.py` cover `bot.py` by domain (shared fakes live in `bot_test_helpers.py`). `test_eval_persona.py` covers the persona eval fixtures. `conftest.py` stubs `BOT_TOKEN`/`GEMINI_API_KEY`/`BOT_LOG_PATH` so the suite needs no real secrets, and empties the remaining provider keys (Munito, Cloudflare, Groq, OpenRouter, Sentry) so a local `.env` cannot send a test to a live API. CI (`.github/workflows/ci.yml`) runs `pyflakes` + `pytest` + `pip-audit` and the proxy checks `deno test` / `deno check` / `deno lint` on every push and pull request, plus a weekly scheduled run; a successful push run triggers `sync-to-hf.yml`, which mirrors the commit to the Hugging Face Space (and refuses to overwrite a direct Space hotfix), and `deploy-proxy.yml`, which ships `proxy/` to Deno Deploy when the tested commit changed it. ## Known limitations - Chat history and quota counters survive redeploys only if Upstash is configured (see [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md)); otherwise they live on the container's ephemeral disk and reset on every rebuild. - YouTube downloading isn't supported, because Hugging Face Spaces' outbound IPs are blocked at the TLS handshake level. Viewing and analyzing a video by link still works. - TikTok downloading depends on the unofficial TikWM API, not an official TikTok endpoint, and needs the proxy above to work from Hugging Face Spaces at all. - Automated tests exercise streaming against mocked clients; live SSE has been run manually through the smoke bot, but there is no controlled integration test. See the manual smoke-test checklist in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) before touching that code path. ## Security See [SECURITY.md](SECURITY.md) for how to report a vulnerability. Secrets are scrubbed from logs and Sentry events before they leave the container; public Space logs hash long numeric chat/user IDs. The webhook verifies Telegram's secret header, caps body size, and ignores duplicate `update_id` deliveries. `ADMIN_SECRET_SEED` is mandatory in production, so admin keys are never derived from the bot token. ## License MIT — see [LICENSE](LICENSE).