LiteLlmRTKSync (LiteLLM Universal Token & Connection Synchronizer) is the
read-only health guardian for LiteLLM
proxies. It reports virtual keys about to expire, provider keys the catalogue
still trusts but the provider has revoked, and rate-limit ceilings that
contradict each other — three states nobody is told about until a request fails.
Third of the RTKSync family, after 9RTKSync (9Router) and OminiRTkSync (OmniRoute).
The full documentation lives in the project wiki: installation, the complete environment-variable contract, the dashboard, authentication and break-glass recovery, persistent logging, architecture, troubleshooting, rate-limit coherence, and how to size a team against an API tier.
Wiki pages are generated from docs/wiki/ — edit them there and open a pull
request; a push to master republishes the wiki automatically.
9RTKSync and OminiRTkSync renew OAuth credentials: their gateway stores tokens that expire, and nobody notices when one dies until a request fails.
LiteLLM has no consumer OAuth. Its credentials are provider API keys in the model catalogue and virtual keys the proxy issues itself, and its state lives in Postgres behind Prisma — there is no SQLite file to read. So there is nothing to renew here, and everything this tool does is read-only: it reports and validates. Changing a limit, a key or a model is the operator's call, through LiteLLM's own screens.
- Virtual Key Expiry Watch
LiteLLM_VerificationToken.expirespasses in silence, and the first sign is a request failing. Keys already expired and keys inside the renewal margin are reported separately.- An undeclared expiry is reported as undeclared, never as "unlimited".
- Live Provider Credential Validation
- A provider key registered in the model catalogue may already have been revoked; the proxy only finds out when it tries to use it. Each key is checked against its own provider instead of being assumed healthy.
- Rate-Limit Coherence Enforcement
- One rule, applied field by field across
tpm_limit,rpm_limit,max_parallel_requestsandmax_budget: key ≤ team ≤ platform default. - Verified against a real proxy: a team capped at
rpm_limit=60accepts a key declaringrpm_limit=600without complaint. The effective limit is always the most restrictive on the path, so the larger number exists only in the record — whoever configured it believes they have 600 and gets 60. - Upstream report: BerriAI/litellm#40866.
- One rule, applied field by field across
- Administrative API Only, Never the Database
- The Prisma schema changes between releases, and writing to the table would skip the invariants the proxy enforces.
- Built-in Web Dashboard
- Lightweight server on port
9090(published on9093in the test stack), rendered entirely server-side, with findings grouped by severity.
- Lightweight server on port
- Strict Virtual Environment Execution
- All Python execution strictly isolated in dedicated virtual environments both
in Docker containers (
/opt/venv) and in local setups (.venv).
- All Python execution strictly isolated in dedicated virtual environments both
in Docker containers (
| Address | http://localhost:9093 |
| User | admin — or whatever you set in DASHBOARD_USER |
| Password | the value of DASHBOARD_PASSWORD in your .env |
There is no factory password, and that is deliberate: a fixed password shipped in an image is public the moment the image is. You choose it once, in one place:
cp .env.example .env
# edit .env:
DASHBOARD_USER=admin
DASHBOARD_PASSWORD=<the password you choose>Then bring the stack up. That user and that password are what the panel accepts.
On first boot with DASHBOARD_PASSWORD empty, the container generates a
recovery credential and writes it inside the data directory. Read it:
docker exec litellmrtk-sync cat /app/data/.dashboard_recoverySign in as admin with that value, then set a real password on the screen. The
recovery credential keeps working afterwards — it is break-glass, and one that
stopped working the moment you set a password would be useless exactly when you
need it.
Português: o painel pede usuário e senha. O usuário é
admin(ou o que estiver emDASHBOARD_USER) e a senha é a que você definir emDASHBOARD_PASSWORDno.env— não existe senha de fábrica, porque um valor fixo publicado na imagem é uma credencial pública. Se subiu sem definir senha, use o comando acima para ler a credencial de recuperação e entre com ela.
The whole configuration comes from environment variables, read from a .env
next to docker-compose.yml — Compose finds it on its own, with no flag.
make setup # creates .env from .env.example, never overwriting an existing oneAt the end, the target lists exactly which variables were left blank and need filling in. Fill them and bring the stack up.
The .env is never committed, and .env.example carries no secret value —
a value published in an example file is, by definition, a public credential. A
test guarantees that every variable a compose file requires exists in the
example, so that cp .env.example .env never produces an incomplete .env.
The three synchronizers listen on the same port inside the container
(9090) and publish on different host ports, so that all three can run side by
side. The same goes for the gateways: each one has its own.
| Service | Internal port | Published on the host |
|---|---|---|
| 9Router | 20128 |
8081 |
| OmniRoute | 20128 |
8082 |
| LiteLLM | 4000 |
8083 |
| 9RTKSync (dashboard) | 9090 |
9091 |
| OminiRTkSync (dashboard) | 9090 |
9092 |
| LiteLlmRTKSync (dashboard) | 9090 |
9093 |
The article stack (claudegravity) keeps 20128, the default 9Router port.
The repository stacks deliberately move out of that range: that way you can run
the article and all three synchronizers at the same time, with no conflict.
Everything is bound to 127.0.0.1: the gateway carries real credentials and
must not be reachable on the local network. To change any of them, edit the left
side of the mapping in the compose file — the right side is the internal port,
the one the process listens on.
Official multi-architecture Docker images (linux/amd64 and linux/arm64) are published automatically to the GitHub Container Registry (GHCR) on pushes to master with changes in src/. The registry enforces an automated retention policy keeping strictly the last 3 versions:
docker pull ghcr.io/pathbit/litellmrtksync:latestAdd litellmrtk-sync to your docker-compose.yml alongside your LiteLLM proxy.
This is the same shape as docker-compose.example.yml
in the repository — service, container and hostname carry the same name, so the
address you read in one place is the address that resolves:
services:
litellmrtk-db:
# O LiteLLM guarda chaves virtuais, times e orcamentos em Postgres via
# Prisma. Sem banco o proxy sobe sem API administrativa, e e a API
# administrativa que este sincronizador le.
image: postgres:16-alpine
container_name: litellmrtk-db
hostname: litellmrtk-db
networks:
- litellmrtksync-net
restart: unless-stopped
environment:
- POSTGRES_USER=litellm
- POSTGRES_DB=litellm
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?defina POSTGRES_PASSWORD no .env}
volumes:
- litellmrtksync_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U litellm -d litellm"]
interval: 10s
timeout: 5s
retries: 20
litellmrtk-router:
image: ghcr.io/berriai/litellm:main-stable
container_name: litellmrtk-router
hostname: litellmrtk-router
networks:
- litellmrtksync-net
restart: unless-stopped
ports:
# 4000 dentro do container; 8083 no host.
- "127.0.0.1:8083:4000"
environment:
# Sem valor de fallback: um segredo publicado em arquivo de exemplo vira o
# segredo real de toda implantacao que so copiou e colou.
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:?defina LITELLM_MASTER_KEY no .env}
- LITELLM_SALT_KEY=${LITELLM_SALT_KEY:?defina LITELLM_SALT_KEY no .env}
- DATABASE_URL=postgresql://litellm:${POSTGRES_PASSWORD:?defina POSTGRES_PASSWORD}@litellmrtk-db:5432/litellm
- STORE_MODEL_IN_DB=True
# Credencial da UI do LiteLLM. Sem estas duas, a tela de login aceita a
# MASTER_KEY como senha -- ou seja, para ver um painel o operador digita
# a credencial que administra a instalacao inteira.
- UI_USERNAME=${DASHBOARD_USER:-admin}
- UI_PASSWORD=${DASHBOARD_PASSWORD:?defina DASHBOARD_PASSWORD no .env}
depends_on:
litellmrtk-db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request;urllib.request.urlopen('http://127.0.0.1:4000/health/liveliness',timeout=3)\""]
interval: 15s
timeout: 5s
retries: 10
start_period: 40s
litellmrtk-sync:
image: ghcr.io/pathbit/litellmrtksync:latest
container_name: litellmrtk-sync
hostname: litellmrtk-sync
networks:
- litellmrtksync-net
restart: unless-stopped
ports:
# Porta interna 9090, igual nos tres sincronizadores; publicada em 9093.
- "127.0.0.1:9093:9090"
volumes:
- litellmrtksync_data:/app/data
environment:
- LITELLM_URL=http://litellmrtk-router:4000
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:?defina LITELLM_MASTER_KEY no .env}
- SYNC_INTERVAL=${SYNC_INTERVAL:-300}
- REFRESH_MARGIN=${REFRESH_MARGIN:-900}
# Teto da plataforma: nenhum time e nenhuma chave pode declarar acima.
- PLATFORM_RPM_LIMIT=${PLATFORM_RPM_LIMIT:-}
- PLATFORM_TPM_LIMIT=${PLATFORM_TPM_LIMIT:-}
- PLATFORM_MAX_BUDGET=${PLATFORM_MAX_BUDGET:-}
- ENABLE_WEB_DASHBOARD=${ENABLE_WEB_DASHBOARD:-1}
- WEB_PORT=${WEB_PORT:-9090}
- DASHBOARD_USER=${DASHBOARD_USER:-admin}
- DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-}
depends_on:
litellmrtk-router:
condition: service_healthy
healthcheck:
test: ["CMD", "/opt/venv/bin/python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9090/healthz', timeout=3)"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
volumes:
litellmrtksync_db:
litellmrtksync_data:
networks:
litellmrtksync-net:
# Rede propria da stack, com nome explicito. Na rede default, duas stacks no
# mesmo daemon resolvem o mesmo nome curto e nao da para saber a qual
# gateway o sincronizador se conectou.
name: litellmrtksync-netFollowing standard environment isolation, local runs strictly use a Python virtual environment with Python 3.14.7:
git clone https://github.com/pathbit/LiteLlmRTKSync.git
cd LiteLlmRTKSyncpython3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .Copy the official template to create your local .env file (the .env file is strictly ignored by git):
cp .env.example .env# Inspect the proxy once and print every finding
litellmrtksync --status --url http://127.0.0.1:8083
# Run an immediate one-shot inspection pass
litellmrtksync --once
# Run continuous background daemon with web dashboard on port 9090 (published on 9093)
litellmrtksync --daemon--status exits 1 when it finds an incoherent limit, so it drops straight into CI.
| Variable | Default | Description |
|---|---|---|
LITELLM_URL |
http://litellmrtk-router:4000 |
Base URL of the LiteLLM proxy — the router service's name inside the stack |
LITELLM_MASTER_KEY |
(empty) | Master key used to read administrative state. Required; a secret — set it in .env, never in the example |
SYNC_INTERVAL |
300 |
Inspection cycle interval in seconds |
REFRESH_MARGIN |
900 |
How far ahead a virtual key starts being reported as expiring |
CRON_ENABLED |
1 |
Internal scheduler (1 to enable, 0 to disable) |
CRON_INTERVAL |
inherits SYNC_INTERVAL |
Scheduler interval when it should differ from the cycle |
PLATFORM_RPM_LIMIT |
(empty) | Platform-wide requests-per-minute ceiling. No team or key may declare above it |
PLATFORM_TPM_LIMIT |
(empty) | Platform-wide tokens-per-minute ceiling |
PLATFORM_MAX_BUDGET |
(empty) | Platform-wide budget ceiling |
ENABLE_WEB_DASHBOARD |
1 |
Enable the embedded web dashboard (1 to enable, 0 to disable) |
WEB_PORT |
9090 |
HTTP port for the web dashboard |
WEB_HOST |
0.0.0.0 |
Network binding interface for the dashboard web server |
DASHBOARD_USER |
admin |
HTTP Basic Auth username for web dashboard access |
DASHBOARD_PASSWORD |
(empty) | Panel password. Left empty, the first sign-in uses the recovery credential generated on first boot |
DASHBOARD_RECOVERY_HASH |
auto | Break-glass credential hash. Generated on first boot when omitted |
CREDENTIAL_CHECK_ENABLED |
1 |
Ask each provider whether the key declared on the model is still accepted |
CREDENTIAL_CHECK_TIMEOUT |
8 |
Timeout in seconds for each credential probe |
DATA_DIR |
/app/data |
Base directory for everything this container writes. Created on startup |
LOG_DIR |
<DATA_DIR>/logs |
Directory for persistent logs |
LOG_RETENTION_DAYS |
30 |
Days of log history to keep |
LOG_LEVEL |
INFO |
Minimum level written to the log |
LOG_TO_STDOUT |
1 |
Also write the log to stdout (1/0) |
Leaving the three PLATFORM_* values empty is a choice, not an error — the panel
reports "no cap declared" and moves on.
When running with ENABLE_WEB_DASHBOARD=1, access the dashboard in your browser:
Dashboard capabilities:
- Live operational metrics (virtual keys, teams, models, findings by severity).
- Expiry countdown per virtual key, with an undeclared expiry shown as undeclared.
- Rate-limit coherence report naming the field, both values and the consequence.
- Proxy liveness card against
/health/liveliness, with the measured latency and the proxy base URL; the panel's own/healthzanswersOKorLITELLM_UNREACHABLE. - English, Portuguese and Spanish in the flag selector, stored server-side so the choice survives a browser change.
- Background scheduler (
CRON_ENABLED/CRON_INTERVAL) with a per-cycle log of what it found, readable from the panel itself. - One cycle trigger and one only (
POST /acoes/cron), which is also what the header's primary button submits, page reload (POST /acoes/atualizar), on-demand proxy probe (POST /acoes/testar-gateway) and full state as JSON (GET /api/status,GET /api/cron-status). - One
(i)button per table row, opening a modal with the full detail — virtual key limits, budget ceiling and expiry instant; a model's API base and where its credential comes from. Never the key itself.
You can run the test suite with zero installations on your host machine (Docker only), or locally via your virtual environment.
The only requirement is Docker. Nothing else needs to be installed on your machine:
# Against a real LiteLLM + Postgres stack
docker compose -f docker-compose.test.yml up -dIf you prefer testing directly on your host with Python 3.14+:
source .venv/bin/activate
make test
# Or directly
PYTHONPATH=src python3 -m unittest discover -s tests -p "test_*.py"Validates that the generated Docker image boots cleanly, exposes the web dashboard on port 9090, responds on /login (200 OK) and /healthz, and executes CLI commands:
# Install package with test dependencies
pip install ".[test]"
# Run Testcontainers validation suite
python3 -m unittest tests/test_container.py -vInherited from the siblings, for the same reasons:
- the page is rendered entirely on the server — no endpoint serves proxy state to an unauthenticated browser;
- security headers, including a Content-Security-Policy, on every response,
the
401body among them — that body is what the browser shows when you press ESC on the Basic Auth dialog; - no credential ever reaches a response body, a log line or a banner: a key with no alias is shown masked, and a model reports whether it has a key, never which;
- a cross-origin
POSTis refused, because the browser attaches Basic Auth to a third-party form on its own; - there is no factory password: a static default is a public credential by definition.
- The
masterbranch is protected. All contributions must be submitted through Pull Requests and pass all CI checks. - For bug reports or new provider requests, please open an issue in GitHub Issues.
- Official upstream proxy: LiteLLM on GitHub.
Distributed under the MIT License. The full text is available in LICENSE.
In practice: use, copy, modify, and distribute freely, including commercially, provided that copyright and license notices accompany copies. The software is provided as is, without warranty.
Terceiro da família RTKSync. Não é cópia dos irmãos: o LiteLLM não tem OAuth de consumidor, e o estado dele vive em Postgres via Prisma, não num SQLite em disco. Aqui não há o que renovar — há o que verificar, e três coisas que ninguém verifica sozinho:
- uma chave virtual vence em silêncio, e o primeiro sinal é a requisição falhando;
- uma chave de provedor cadastrada num modelo pode ter sido revogada, e o proxy só descobre na hora de usar;
- um limite de chave acima do limite do time é aceito sem reclamação —
verificado contra um proxy real: um time com
rpm_limit=60aceita uma chave declarandorpm_limit=600. Vale sempre o teto mais restritivo do caminho, então o número maior existe só no cadastro, e quem configurou acredita ter 600 e recebe 60.
Tudo aqui é somente leitura. A ferramenta relata e valida; alterar limite, chave ou modelo é decisão do operador, pelas telas do próprio LiteLLM.
Coerência de limites: uma regra só, campo a campo em tpm_limit,
rpm_limit, max_parallel_requests e max_budget — nenhum nível pode declarar
valor maior que o de cima: chave ≤ time ≤ padrão da plataforma. O teto da
plataforma vem de PLATFORM_RPM_LIMIT, PLATFORM_TPM_LIMIT e
PLATFORM_MAX_BUDGET; deixá-los vazios é uma escolha, não um erro.
Documentação completa: wiki do projeto, gerada de
docs/wiki/ — instalação, contrato de variáveis, painel,
autenticação e recuperação, log persistente, arquitetura, diagnóstico, coerência
de limites e como dimensionar um time contra um tier de API.
Como rodar: copie .env.example para .env, preencha a master key e os
tetos, e suba com docker compose -f docker-compose.example.yml up -d. O painel
responde em http://127.0.0.1:9093, preso ao loopback. O primeiro acesso usa a
credencial de recuperação, gerada no primeiro boot — o log diz em qual arquivo
ela está, nunca o valor. Defina a sua senha pela tela: não existe senha de
fábrica, porque um valor estático é, por definição, uma credencial pública.
Developed with ❤️ by Pathbit