Self-hosting Sophie

Last updated: October 20, 2026

This guide explains how to self-host Sophie Bot using Podman and Ansible, mirroring the official production environment.

Architecture Overview

Sophie is designed to run as a set of microservices to ensure scalability and high availability:
  • Stable Instance: The primary bot instance that handles most user interactions. It can also act as a proxy to the Beta instance.
  • Beta Instance: A secondary instance used for testing new features.
  • Scheduler: Handles background tasks, timed events, and scheduled operations.
  • REST API: Provides an interface for the web dashboard and external integrations.
All components are containerized and typically run using Podman.

Prerequisites

Before starting, ensure you have the following installed on your host system:
  • Podman: For container management.
  • Ansible: For automated deployment.
  • MongoDB: Persistent data storage.
  • Redis / Valkey: For caching and FSM (Finite State Machine) storage.

Deployment with Ansible

The recommended way to deploy Sophie is using the provided Ansible playbooks in the
deploy/
directory.

1. Configuration

Copy
data/config.example.env
to
data/config.env
and fill in the required values. Key variables include:
  • TOKEN
    : Your Telegram Bot API token.
  • MONGO_HOST
    : Connection string for MongoDB.
  • REDIS_HOST
    : Hostname for Redis.

AI telemetry with Sentry

Set
SENTRY_URL
to a Sentry DSN to enable error reporting and performance traces. With a DSN, Sophie traces every transaction by default (
SENTRY_TRACES_SAMPLE_RATE=1.0
); lower this rate to reduce volume, or set it to
0
to disable traces while keeping error reporting. Sampling is at the parent transaction level, so the same rate also applies to non-AI requests. The Pydantic AI integration records agent/model/tool spans, token usage, prompts, replies, and tool inputs/outputs in captured traces. These can contain personal data and produce high Sentry volume; set an appropriate retention policy. For Ansible deployments, set
SENTRY_TRACES_SAMPLE_RATE
in the operator environment to override the default in all bot, scheduler, and REST processes.
Sophie also adds spans for its own AI caches and state transitions. AI request metrics still use the existing
METRICS_ENABLE
and
SENTRY_ENABLE_METRICS
settings.

2. Run the Playbook

To deploy the stable environment:
Loading...
To deploy the beta environment (includes scheduler and REST API):
Loading...

Telegram Mini App authentication

Mini App login at
/auth/login/tma
validates Telegram init data with the bot token using
init-data-py
v1. Init data has an explicit one-day lifetime; older credentials are rejected, so clients must reopen the Mini App to obtain fresh init data rather than reuse it indefinitely. Malformed init data (including missing required fields) or validated data without a user returns HTTP 400. Invalid hashes and expired credentials return HTTP 401. A valid Telegram user not registered in Sophie's database still receives HTTP 403. The successful token response is unchanged.

Greetings and welcome security

Welcome messages go only to the members they greet, one message per member with their own name. Nothing is posted to the chat, so clean-welcome cleanup has nothing to delete.
Captcha prompts also go to each new member as ephemeral messages, rather than into the chat. Only new members see them, one prompt each; nothing is left to delete after the captcha is passed.
If a member leaves before Telegram accepts their ephemeral welcome or captcha prompt,
send_saveable
returns
None
and logs
outcome=skipped
,
reason=recipient_unavailable
, the chat ID and the recipient ID. This applies to Rich messages, traditional text and single-media saveables. A missing reply target is retried without the reply, but always with the same ephemeral recipient; a departed recipient is never retried publicly. Only the exact
USER_NOT_PARTICIPANT
failure on an ephemeral send is treated this way: unrelated failures and this error on public sends are still surfaced.
A prompt whose security note is an album is still posted to the chat:
sendMediaGroup
cannot address one member, and splitting the album into separate ephemeral messages is no way around it — Telegram accepts at most five ephemeral messages per user.

AI models and providers

Sophie's AI models, the endpoints they are served from and the API keys used to reach them live in the database, not in the configuration file. They are managed at runtime with operator commands, so adding a model or rotating a key never needs a redeploy.

Seeding

The
seed_ai_catalog
migration creates catalog models and the
openrouter
provider; the
seed_vendor_sdk_provider_keys
migration creates
mistral
and
openai
providers for moderation and transcription. All three start without keys. Configure credentials in the database before enabling AI, using
/op_ai_provider <name> ^key=<your-key>
in a private chat with the bot or updating the provider through the operator API.
The initial catalog also includes a
qwencloud
model. To use it, create an OpenAI-compatible provider named
qwencloud
with its endpoint and key via
/op_ai_provider
or the operator API. No AI provider credentials are read from environment variables.

Managing the catalog

Command

Purpose

/op_ai_providersList providers. API keys are always masked.
/op_ai_provider <name> ^kind= ^base_url= ^key= ^enabled=Create or update a provider. Private chat only; the command message is deleted immediately.
/op_ai_modelsList models and what each one is used for.
/op_ai_model <name> ^provider= ^api_name= ^role= ^unrole= ^reasoning= ^context_window_tokens= ^enabled=Create or update a model.
kind
is
openrouter
,
openai_compatible
, or
moderation
(a key for a vendor SDK rather than a chat-completions endpoint — do not point models at one). A role is
<mode>:<purpose>
— for example
^role=support:chatbot
— or just
<purpose>
for the purposes that are not per-chat (
summary
,
moderation_reason
). Purposes are
chatbot
,
translation
,
filters
,
summary
and
moderation_reason
,
sophie_inspect
; modes are
entertainment
,
moderation
,
support
,
sophie_pm
and
sophie_help
.
A mode with no model for a purpose falls back to the
support
tier, so you only need to define the roles you want to differ. Changes take effect on every process within a few seconds without a restart.
context_window_tokens
is the model's authoritative positive integer context capacity. The
add_ai_model_context_sizes
migration fetches the public OpenRouter models API once, matching exact
api_name
first and exact catalog
name
second. Existing configured capacities are preserved; unmatched models stay undefined and are reported. Set those manually with
/op_ai_model <name> ^context_window_tokens=<tokens>
or the operator API. Network or invalid-response errors stop the migration before it changes any capacities. Rollback removes only unchanged migration-added values and preserves later operator edits.
Modern context requires a configured capacity for every possible chatbot candidate, including the last-resort model. Missing capacities stop the request with an AI configuration failure reply and are reported for diagnosis; no provider-profile or model-name estimate is used. Legacy context can still use models with undefined capacities.

AI usage and research settings

Quota accounting and provider-debug output use Pydantic AI's native token counts. Zero input or output tokens remain zero; pricing uses the actual input/output split rather than estimating a missing side.
Research reads
ai_research_service_tier
with the other workflow settings once per workflow. That tier is the default for every research model attempt; a tier configured on the model's catalog role can override it.

AI moderation availability

The existing
ai_moderation
flag and chat mode control moderation. Mistral and OpenAI classifier requests retain their normal retry budget. If a supported transient provider failure (including overload/rate limits, service outages or transport timeouts) exhausts that budget, Sophie logs the failure without sending a chat message and stops processing the current message across bot handlers. An unclassified message is not a clean moderation result: it stays in the chat, and its author is not banned or restricted because of the failed check. The message is not added to AI history. There is no deferred classification or extra retry.
Failures are logged with operation, model and error type. When Sentry is configured, the log includes a reference to a structured provider-failure event. Moderation retry breadcrumbs and terminal events omit provider response text and SDK tracebacks, which can contain the private classifier input. Persistent authentication/configuration errors and unexpected programming errors still follow the normal error-reporting path; they are not presented as temporary classifier unavailability.

AI progress

Chatbot replies show an in-progress message while they stream. Manual translation (
/tr
,
/translate
) and
/research
also show progress. All three use STFU Rich rendering with a fixed animated AI emoji and a three-emoji footer; translation and research edit the same Rich message for the result. Automatic translation stays silent until its result is ready. The chatbot and manual translation initially show a random working message after the animated AI emoji. When reasoning or an activity begins, that space stays empty until answer text streams in; progress appears below. Streamed reasoning appears once as an italic “Reasoning...” activity below the header, without revealing reasoning text. Later reasoning passes, including those after tool calls, do not add another activity. Every tool invocation and retry stays in the bottom activity list, including repeated calls and tools hidden from the final header. New streamed answer text clears the activity list. Later tool calls appear below the answer text already shown; the next text update clears those entries in turn.
Manual
/tr
and
/translate
retain their current activity list until the final translation replaces the progress message. A replied voice starts with “Transcribing voice message...” before transcription, followed by “Translating...”. Replied images and videos show processing stages (including video audio transcription) before translation. Chatbot replies stack the same media stages during context preparation.
Video transcription reads the first audio stream even when the container lists a video stream before it.
Completed replies replace the animation with a static AI emoji, eligible used-tool titles without tool icons (Search first when used), and a battery footer in its own paragraph. Each tool has its own
display_in_ai_header
setting in
sophie_bot/modules/ai/utils/ai_tool.py
; set it to
False
to hide that tool from the final header without hiding its in-progress activity. Custom emoji IDs remain in the tool metadata but are not rendered in the progress activity or completed header. The low, middle, and high battery icons correspond to 0–32%, 33–65%, and 66–100%.
ai_chatbot_show_model_name
adds the model beside the battery reading. Only the assistant's answer is stored in conversation history; the displayed header, tool titles, and battery footer are not. Legacy context replays recent tool calls and results, with each stored result capped by
ai_chatbot_tool_history_max_chars
. Proactive replies use the same inline custom-emoji header and bottom battery footer. Header styles are no longer configurable.

Modern AI context

Modern history is opt-in. Run the model-context-size migration and configure every unmatched model, including last-resort candidates, before enabling it:
Loading...
ModernContext
keeps one serialized session per chat, topic and AI mode. It appends new chat messages and native assistant/tool exchanges once, rather than rebuilding the complete recent-message block on every turn. When the budget is reached, it deletes old background batches first, then whole completed turns. Tool calls and returns stay paired. There is no generated summary or compaction message; an oversized current request fails instead of being truncated.
Admission counts model-facing instructions, text and tool arguments/results with each candidate's local tokenizer, not the serialized session JSON. Known OpenAI model names use
tiktoken
(including
gpt-5.6-luna
/
openai/gpt-5.6-luna
, using the GPT-5
o200k_base
mapping). GLM-5.3 and GLM-5.3-Flash use Hugging Face
tokenizers
with only
zai-org/GLM-5.3
's
tokenizer.json
, pinned to revision
ba32f8c19c8738e2c57c71798c601d477a7bf844
; provider prefixes and routing suffixes on both model names are accepted. Unrecognized model names use the bundled OpenAI
cl100k_base
tokenizer as an estimate.
UNKNOWN_TOKENIZER_CONTEXT_CAP=8192
and
UNKNOWN_TOKENIZER_INPUT_PERCENT=80
in
ai_tokens.py
define an application policy: limit exposure to estimation error by capping the registered capacity at 8192 and keeping 80% of the input left after output and tool-schema reserves. These chosen safety limits are not a measured tokenizer ratio or a provider context-window claim; models with known tokenizers use their registered capacities. A smaller registered window still wins, and exhausted reserves leave zero input capacity, so admission rejects that candidate. This policy cannot guarantee a fit for an unknown tokenizer; register a supported tokenizer to use a model's larger window confidently. No model weights or remote token-counting API are used.
Persisted history is pruned with the primary model's tokenizer and capacity. Each attempted candidate, including image-eligible fallbacks and the last-resort model, gets an independently budgeted request projection; smaller fallback windows never delete primary session history. Runtime snapshots are restored in the projection if pruning removes the turn carrying them. An oversized current request fails before it reaches that candidate's provider.
The default
ai_chatbot_modern_context_tokens=0
uses the registered model capacity minus reserves. Positive values remain explicit input caps, including existing chat and global overrides; clear these overrides with
/op_ff ... unset
to use the model-aware default. This setting does not replace the registered capacities. Admission also reserves the configured response limit (2048 tokens when unlimited), 4096 tokens of safety/tool-schema headroom, and per-message/part framing allowances. Each image reserves 4096 estimated tokens, independently of its transport size. These are conservative allowances, not a guarantee of exact provider usage: chat templates, image handling and hidden provider additions can differ. Stored event costs are recounted on each build, so changing models or tokenizers does not reuse stale token estimates.
Cache retention is separate from these budgets: the shared message cache expires after 48 hours, but retained native session events are pruned by tokens, not age. The modern builder reads the eligible cache without a latest-10 seed cap; legacy/count-limited seed policies do not control native session growth. Changes to cold-start seed selection or 48-hour cache retention are deferred.
Admission budgets the history before a run; newly generated tool results can grow beyond the reserved headroom. It does not impose a new per-tool-step truncation policy.
The runtime Docker image bundles the pinned GLM tokenizer and every OpenAI encoding provided by its installed
tiktoken
version during the dependency build stage. Loading a tokenizer never downloads assets at runtime and does not require writable caches. Missing or invalid assets for a recognized model fail the request rather than silently changing its token estimate.
For source-checkout development, prepare the same assets once with network access:
Loading...
Generated files live in
sophie_bot/tokenizer_data/
and are excluded from Git and the Docker build context. CI test setup runs the same generator and smoke-checks the required tokenizers before running pytest. The image always generates its own assets from the locked dependencies and pinned GLM revision. No model weights or provider credentials are needed. Tokenizer instances load off the event loop and are cached per process.
Stable instructions precede conversation history. Each request carries one current runtime snapshot with the date, escaped XML chat notes and 1-based XML memory items; earlier runtime blocks are removed structurally from replayed history. Previous-chat summary context uses only the newest stored daily summary for that chat, with its
summary_date
rendered in both runtime XML and chatbot instructions. An empty or missing latest summary clears that context without generating a replacement. Current/replied speakers and memory references can appear in the entertainment roster; old private alias maps do not grow the roster forever. Group and channel titles are included as escaped XML in runtime snapshots, never in stable instructions. Private-chat names are omitted from the conversation-title field.
Speakers and message references use session-local
uN
and
mN
labels. Typed builders select these labels instead of real handles or Telegram IDs for attribution fields. Entertainment alone includes literal first-name attribution. Its outgoing first-name mentions resolve to usernames only when the session identity lookup is unambiguous; known ambiguous names remain plain. Other modern modes keep alias-only attribution. Authored messages, notes, summaries, configuration prompts and arbitrary native tool data are not scanned or redacted. Only XML syntax is escaped. Native provider message parts and signatures replay unchanged; there is no generic sanitization processor. Memory facts use identity anchors, stored as internal links and re-aliased for a new session, so first-name jokes do not attach to a different person after reset.
Supported providers receive caching options: Anthropic-compatible OpenRouter models use explicit cache markers, native Anthropic uses its cache controls, and native OpenAI uses an opaque session cache key. Generic compatible endpoints receive no unsupported OpenAI cache fields. Actual cache reads/writes are recorded from provider usage; cache hits remain provider-dependent.
/ai_reset
clears the shared message cache and native sessions in one epoch-fenced transaction. Pre-reset incoming messages and modern deliveries cannot repopulate it after their handler or generation finishes. Tool history and memory are also cleared. Switching the flag off uses
OldContext
; it does not reinterpret the modern session. After a deployment that retires old display headers, reset affected histories if previously decorated cache entries remain.

AI moderation

The AI moderator classifies messages against nine categories and deletes anything that crosses a threshold. It does not go through the AI catalog above — it calls a dedicated moderation classifier, chosen with the
ai_moderation_provider
feature flag:

Value

Model

Catalog provider holding the key

mistral (default)mistral-moderation-latestmistral
openaiomni-moderation-latestopenai
Switching backend is two steps: put the key in the catalog, then flip the flag. Neither needs a restart — the client is rebuilt as soon as the catalog version changes.
Loading...
The two providers report different categories, and Sophie normalises them onto its own nine.
health
,
financial
,
law
and
pii
have no OpenAI equivalent and never trigger on that backend.

Per-chat detection levels

Chat admins run
/ai_moderator
to get a table of the nine categories and a button for each one. Pressing a button walks that category through Off → Low → Medium → High.
The level multiplies the classifier's score before it is compared to the threshold, so a chat can be made more or less sensitive without an operator retuning anything. The factors are feature flags:

Level

Flag

Default

Lowai_moderation_level_low_multiplier0.7
Mediumai_moderation_level_normal_multiplier1.0
Highai_moderation_level_high_multiplier1.3
Off skips the category entirely rather than scaling it to zero.

Tuning thresholds

Every threshold is a feature flag, so it can be changed per chat with no redeploy:
Loading...
Flags are named
ai_moderation_threshold_<provider>_<the provider's own category>
, because categories that Sophie groups together do not score alike:
sexual/minors
needs a much lower cut-off than
sexual
. These are the operator-level tuning; per-chat sensitivity is the detection level above.
The "message deleted" notice removes itself after
ai_moderation_notice_delete_after_seconds
(30 by default); set it to
0
to keep the notices in the chat.

Source inspection

The Sophie-help assistant can start a sub-agent that reads Sophie's own source code when the documentation cannot answer a question. It costs several model requests per question, so usage is limited per chat.
Groups do not get it from their AI mode.
ai_sophie_inspect_chats
is a space or comma separated list of group IDs allowed to use it anyway, for the chats where people ask how Sophie works.
Every run is bounded:
ai_sophie_inspect_request_limit
,
ai_sophie_inspect_tool_calls_limit
and
ai_sophie_inspect_output_tokens_limit
cap one run,
ai_sophie_inspect_daily_chat_limit
caps how many runs one chat may start per day, and the tokens are charged to that chat's AI quota like any other feature.
The model it uses is the catalog model holding the
sophie_inspect
role, so it is swapped like any other:
/op_ai_model <name> ^role=sophie_inspect
. Prefer a cheap one — the daily cap is what bounds the damage, not the price per run.
The sub-agent can only read
.py
files inside the
sophie_bot
package, and only through search and bounded reads — it never executes anything and never sees configuration or data.

Running with Podman (Manual)

If you prefer to run containers manually, you can use the following logic (based on the Quadlet templates):

Stable Instance

Loading...

Scheduler

The scheduler is the same image but runs with
MODE=scheduler
.
Loading...

REST API

The REST API is the same image but runs with
MODE=rest
.
Loading...

Proxy System (Stable + Beta)

Sophie implements a unique proxying system where the Stable instance can redirect traffic to the Beta instance. This allows seamless transitions and testing of new features.
  • PROXY_ENABLE
    : Set to
    True
    to enable proxying.
  • PROXY_STABLE_INSTANCE_URL
    : URL of the stable instance.
  • PROXY_BETA_INSTANCE_URL
    : URL of the beta instance.
When enabled, the bot can route requests between instances based on configuration, allowing for "canary" style deployments or easy beta testing for specific users/chats.

Environment Variables Reference

Variable

Description

TOKENTelegram Bot Token
MONGO_DBMongoDB Database Name
REDIS_DB_FSMRedis Database index for FSM
OWNER_IDTelegram User ID of the bot owner
ENVIRONMENTName of the environment (e.g., production-stable)
MODESet to scheduler for the scheduler service