Files
Portainer/litellm/README.md
T
2026-08-24 12:13:38 +00:00

24 KiB

LiteLLM

LiteLLM es la pasarela central de modelos del repositorio. Expone una API compatible con OpenAI a consumidores como Open WebUI, n8n y Paperless AI y centraliza las credenciales de los proveedores.

Este servicio se administra directamente con Docker Compose. No depende de Portainer y no debe desplegarse usando stack.env.

Arquitectura

Open WebUI / n8n / Paperless AI / clientes externos
                    |
                    | API OpenAI-compatible
                    | LITELLM_MASTER_KEY
                    v
              LiteLLM :4000
                |        |
                |        +----> DeepSeek API
                |
                +----> codex_provider.py
                |            |
                |            +----> Codex backend API
                |            +----> auth.json de Codex
                |
                +----> claude_provider.py
                |            |
                |            +----> api.anthropic.com/v1/messages
                |            +----> .credentials.json de Claude Code
                |
                +----> PostgreSQL 16

Los consumidores nunca reciben una credencial de DeepSeek ni los tokens de Codex o Claude. Sólo conocen la URL de LiteLLM, una clave de LiteLLM y un alias de modelo.

Contenido de la carpeta

Archivo Función
docker-compose.yml Define LiteLLM, PostgreSQL, redes y montajes.
.env Configuración real local. Contiene secretos y no está versionado.
stack.env Plantilla histórica versionada. No usar para desplegar.
config.yaml Catálogo declarativo de modelos y registro de proveedores personalizados.
codex_provider.py Adaptador entre LiteLLM y la API Responses usada por Codex.
claude_provider.py Renovación OAuth de Claude y delegación al proveedor Anthropic nativo.
README.md Arquitectura, operación, seguridad y diagnóstico.

Servicios y persistencia

litellm

  • Imagen fijada mediante digest de ghcr.io/berriai/litellm.
  • Arranca con --config /app/config.yaml.
  • Escucha en el puerto interno 4000; no publica un puerto directamente en el host.
  • Está unido a la red interna litellm y a la red externa proxy.
  • Guarda configuración y estado en PostgreSQL porque STORE_MODEL_IN_DB=True.
  • Reinicia con la política unless-stopped.

Montajes:

./config.yaml                      -> /app/config.yaml          (solo lectura)
./codex_provider.py                -> /app/codex_provider.py    (solo lectura)
./claude_provider.py               -> /app/claude_provider.py   (solo lectura)
/home/felidae/.codex/auth.json     -> /root/.codex/auth.json    (lectura/escritura)
/home/felidae/.claude/.credentials.json
                                   -> /root/.claude/.credentials.json (lectura/escritura)

Los dos archivos de autenticación son deliberadamente escribibles porque los proveedores renuevan tokens y persisten las sesiones actualizadas. Esto también significa que el contenedor tiene acceso a los refresh tokens de ChatGPT y Claude.ai.

litellm-db

  • Usa PostgreSQL 16.
  • Sólo pertenece a la red interna litellm.
  • No publica 5432 en el host ni se une a proxy.
  • Persiste sus datos desde LITELLM_DB_DATA_PATH.
  • Se resuelve dentro de la red como litellm-db.

depends_on establece el orden de arranque, pero no espera a que PostgreSQL esté listo. Actualmente ninguno de los dos servicios tiene un healthcheck de Docker.

Variables de entorno

El .env local debe definir:

Variable Función
LITELLM_MASTER_KEY Autentica a los clientes de la API de LiteLLM.
LITELLM_SALT_KEY Cifra datos sensibles guardados por LiteLLM; debe permanecer estable.
DEEPSEEK_API_KEY Credencial usada por los alias thsllm-*.
POSTGRES_USER Usuario de PostgreSQL.
POSTGRES_PASSWORD Contraseña de PostgreSQL.
POSTGRES_DB Base de datos de LiteLLM.
LITELLM_DB_DATA_PATH Ruta persistente del host para PostgreSQL.
LITELLM_DOMAIN Dominio público documentado para el servicio.

No imprimas el contenido de .env durante verificaciones. Comprueba sólo que las variables existen:

awk -F= '/^[A-Za-z_][A-Za-z0-9_]*=/{print $1}' .env

stack.env contiene valores de plantilla. Aunque permanece en el repositorio por compatibilidad histórica, el despliegue soportado usa exclusivamente .env mediante --env-file .env.

Redes y exposición con Traefik

Los contenedores conectados a proxy pueden acceder por DNS interno a:

http://litellm:4000/v1

El dominio público es:

https://llm.sherlockhomeless.net

El enrutamiento vive fuera de esta carpeta, en /opt/traefik/dynamic/litellm.yml:

  • El router litellm-ui protege la interfaz mediante Authentik y CrowdSec.
  • El router litellm-api expone /v1, /health, /key y /model sin Authentik y aplica el middleware de CrowdSec.
  • La API depende de la validación de LITELLM_MASTER_KEY realizada por LiteLLM. Authentik no sustituye esa clave.

Las rutas administrativas /key y /model forman parte de la superficie pública. Antes de ampliar el uso del servicio conviene confirmar qué operaciones permite cada versión de LiteLLM y restringirlas en Traefik si no son necesarias.

Catálogo de modelos

Declarado en config.yaml

Codex mediante el proveedor personalizado:

  • gpt-5.6-sol
  • gpt-5.5
  • gpt-5.3-codex
  • gpt-5.4-mini
  • gpt-5.2

DeepSeek mediante su API:

  • thsllm-chat
  • thsllm-reasoner
  • thsllm-v4-pro

Claude mediante la sesión OAuth de Claude Code:

  • claude-sonnet-5

Aunque la cuenta OAuth puede enumerar otros modelos Anthropic, sólo Sonnet 5 se publica deliberadamente a los consumidores de LiteLLM.

Los nombres thsllm-* evitan exponer el proveedor real en los consumidores y permiten cambiar el backend manteniendo un alias estable.

Para los modelos Codex se usa internamente el prefijo cx-. Esto evita que LiteLLM los clasifique como modelos OpenAI nativos. codex_provider.py vuelve a convertir cx-* en gpt-* antes de enviar la petición.

Los modelos Claude usan internamente claudeoauth/clx-*. El proveedor personalizado renueva la sesión, convierte clx-* en claude-* y delega la traducción de mensajes, herramientas y respuestas al proveedor Anthropic nativo de LiteLLM. Los reintentos están desactivados para estos modelos y así un 429 de la suscripción no multiplica solicitudes contra la misma cuota.

Catálogo efectivo

LiteLLM puede conservar modelos en PostgreSQL. Por eso config.yaml no es necesariamente la única fuente de verdad cuando STORE_MODEL_IN_DB=True.

En la revisión del 16 de agosto de 2026, /v1/models devolvía además:

  • thsllm-flash
  • thsllm-pro

Esos alias no aparecen en el YAML actual y proceden previsiblemente del estado persistido en la base de datos. Antes de eliminar o renombrar modelos, revisa tanto config.yaml como el catálogo efectivo de LiteLLM.

Autenticación de Codex

Estado y almacenamiento

La integración usa una sesión de ChatGPT creada por Codex CLI, no una API key de OpenAI Platform. El archivo del host es:

/home/felidae/.codex/auth.json

Codex CLI informa del método activo con:

codex login status

El archivo contiene material reutilizable, incluidos access token, refresh token y el identificador de cuenta. Debe tratarse como una contraseña:

  • No añadirlo al repositorio.
  • No copiarlo a imágenes Docker.
  • No mostrarlo en logs, incidencias o documentación.
  • No incluirlo en backups sin cifrar.
  • No montarlo en Open WebUI, n8n, Paperless ni otros consumidores.
  • Mantener permisos 0600 en el host.

La documentación oficial de OpenAI describe ~/.codex/auth.json como una de las ubicaciones de caché de credenciales de Codex y advierte que contiene tokens de acceso. También recomienda autenticación mediante API key para la automatización general. Referencia: OpenAI Docs: Authentication.

Flujo de una petición

  1. Un cliente llama a LiteLLM con LITELLM_MASTER_KEY y un alias.
  2. LiteLLM valida la clave y selecciona custom_llm_provider: codex.
  3. El proveedor lee CODEX_AUTH_FILE, configurado como /root/.codex/auth.json.
  4. Decodifica localmente el JWT para conocer su expiración.
  5. Si el access token vence en menos de 60 segundos, usa el refresh token contra https://auth.openai.com/oauth/token.
  6. Guarda los tokens renovados en el mismo archivo montado.
  7. Envía la petición a https://chatgpt.com/backend-api/codex/responses con el access token y el identificador de cuenta.
  8. Convierte el flujo SSE recibido en una respuesta compatible con LiteLLM.

La renovación observada estaba operativa durante la revisión del 16 de agosto de 2026. No se documentan aquí fechas de expiración, identificadores ni valores de tokens.

Implicaciones

Este diseño convierte indirectamente la sesión de ChatGPT del usuario felidae en el backend de un proxy compartido. Cualquier consumidor que posea LITELLM_MASTER_KEY puede utilizar esa cuenta y sus límites a través de los alias Codex.

Además, el endpoint chatgpt.com/backend-api/codex/responses, el client_id embebido y las cabeceras de compatibilidad son detalles internos y pueden cambiar sin mantener compatibilidad con este proveedor. Esta ruta no debe confundirse con una integración basada en la API pública de OpenAI Platform.

Para un servicio multiusuario o una automatización crítica, la opción más estable es evaluar la API pública de OpenAI con una API key dedicada y límites propios, manteniendo LiteLLM como pasarela.

Autenticación de Claude

Endpoint y almacenamiento

Claude Code 2.1.233 está autenticado contra el proveedor first-party mediante una cuenta Claude.ai Pro. Su caché OAuth está en:

/home/felidae/.claude/.credentials.json

El estado se comprueba sin mostrar tokens mediante:

claude auth status

El objeto claudeAiOauth contiene accessToken, refreshToken, expiresAt, refreshTokenExpiresAt, scopes y tipo de suscripción. El archivo tiene permisos 0600 y debe tratarse como una contraseña.

La inferencia usa:

POST https://api.anthropic.com/v1/messages
Authorization: Bearer <access token>
anthropic-beta: claude-code-20250219,oauth-2025-04-20

claude_provider.py entrega el token al proveedor Anthropic nativo mediante ANTHROPIC_AUTH_TOKEN; no debe pasarlo como api_key, porque eso añadiría x-api-key y Anthropic rechaza mezclar ambos esquemas. El adaptador añade la identidad mínima de Claude Code (beta, x-app, user-agent y mensaje de sistema) y delega la traducción de mensajes, herramientas y respuestas a LiteLLM. Si falta esa identidad, Anthropic puede responder con un 429 rate_limit_error genérico aunque la misma cuenta funcione desde el CLI.

Desde abril de 2026 Anthropic separa la facturación en dos grupos: las apps oficiales (CLI de Claude Code, claude.ai) consumen los límites del plan, mientras que las aplicaciones de terceros consumen un saldo aparte de "extra usage". Cuando ese saldo está vacío, las peticiones clasificadas como third-party se rechazan con:

400 invalid_request_error:
Third-party apps now draw from your extra usage, not your plan limits.
Add more at claude.ai/settings/usage and keep going.

La clasificación la decide la identidad HTTP de la petición, y hay tres detalles que la delataban como third-party:

  1. El user-agent decía (external, sdk-cli). El binario de Claude Code usa (external, cli) cuando corre como CLI; sdk-cli es el entrypoint del SDK (CLAUDE_CODE_ENTRYPOINT) y Anthropic lo factura como terceros.
  2. Se enviaba anthropic-dangerous-direct-browser-access: true, una cabecera que sólo usa la web de claude.ai y que también marca la petición como terceros.
  3. LiteLLM 1.82 reescribe anthropic-beta contra una lista blanca (anthropic_beta_headers_config.json) y descartaba claude-code-20250219, que no figura en ella y es la beta claude_code del CLI.

claude_provider.py corrige los tres puntos: identifica cli en el user-agent, omite la cabecera de navegador y registra claude-code-20250219 en la lista blanca al importarse (_patch_beta_headers). Si el error vuelve a aparecer, revisa que esas cabeceras se conserven en el registro de LiteLLM y que la cuenta tenga saldo de "extra usage" en claude.ai/settings/usage.

Renovación

Cinco minutos antes de expiresAt, el proveedor envía el refresh token a:

POST https://platform.claude.com/v1/oauth/token

La petición usa el flujo refresh_token y el identificador público del cliente Claude Code. El refresh token puede rotar; por eso el archivo está montado como lectura/escritura. La escritura se realiza bajo un lock exclusivo, conserva el resto del JSON, trunca el contenido anterior y fuerza la sincronización a disco.

El lock evita dos renovaciones concurrentes dentro del contenedor. No coordina formalmente con una versión de Claude Code que ignore ese mismo advisory lock; evita ejecutar claude auth login o claude auth logout mientras LiteLLM esté renovando la sesión.

Estado soportado

Anthropic documenta que Claude Code utiliza api.anthropic.com y documenta LiteLLM como gateway, pero la configuración soportada del gateway usa API keys, Amazon Bedrock o Google Vertex AI. Montar la sesión OAuth de una suscripción Claude.ai dentro de un proxy es una integración experimental y no está documentada por Anthropic como arquitectura de producción.

Referencias:

Para un servicio multiusuario o crítico debe preferirse una ANTHROPIC_API_KEY dedicada, Bedrock o Vertex AI. Compartir LITELLM_MASTER_KEY concede acceso indirecto a la cuota de la suscripción Pro.

Compatibilidad

La traducción principal la realiza LiteLLM, por lo que conserva más funciones Anthropic que el proveedor Codex personalizado. La capa OAuth filtra temperature, porque los modelos Claude 5 actuales lo rechazan aunque algunos clientes OpenAI-compatible lo envían por defecto.

El streaming personalizado normaliza texto, razón de finalización y uso. Debe probarse expresamente el streaming con herramientas antes de depender de tool calls complejas, porque esa normalización puede no conservar todos los deltas especializados de Anthropic.

Comportamiento y límites de codex_provider.py

El proveedor implementa llamadas síncronas, asíncronas y streaming. El backend se consume siempre mediante streaming; en llamadas no streaming se agregan los fragmentos antes de devolver la respuesta.

La traducción actual sólo conserva principalmente:

  • Mensajes de usuario y asistente.
  • Mensajes system y developer, unidos como instructions.
  • Nombre del modelo.
  • reasoning_effort, con valor predeterminado medium.
  • Texto generado y contadores básicos de tokens.

No es una implementación completa de Responses ni de Chat Completions. Entre otros, no reenvía explícitamente herramientas/function calling, límites de tokens, temperatura, formatos estructurados ni la mayoría de parámetros opcionales. El parser SSE sólo procesa deltas de texto y el evento final de uso. Un cliente puede ser compatible con la API de LiteLLM y aun así perder funciones al usar un alias Codex.

Riesgo de concurrencia en la renovación

La renovación hace una lectura y una escritura directa de auth.json, sin bloqueo entre peticiones y sin reemplazo atómico. Dos solicitudes simultáneas cerca de la expiración podrían intentar rotar el mismo refresh token. Una interrupción mientras se escribe también podría dejar el JSON incompleto.

Si el servicio va a recibir concurrencia significativa, el proveedor debería:

  1. Serializar la renovación mediante un lock.
  2. Escribir primero un archivo temporal con permisos restrictivos.
  3. Sincronizarlo y reemplazar auth.json atómicamente.
  4. Releer el archivo después de adquirir el lock por si otra petición ya lo renovó.
  5. Evitar que errores y cuerpos HTTP incluyan tokens en logs.

Consumidores

Un cliente compatible con OpenAI necesita:

Base URL: http://litellm:4000/v1
API key: valor de LITELLM_MASTER_KEY
Model: uno de los alias publicados

Usa la URL interna cuando ambos contenedores compartan proxy. Para clientes externos usa HTTPS. Nunca configures un token de Codex o Claude como API key de un consumidor.

Flujo de Paperless AI:

Paperless -> Paperless AI -> LiteLLM -> proveedor seleccionado -> LLM

Open WebUI y n8n siguen el mismo patrón desde la red proxy.

Despliegue

Desde esta carpeta:

docker compose --env-file .env config
docker compose --env-file .env up -d
docker compose --env-file .env ps

No usar:

docker compose --env-file stack.env up -d

Tras modificar config.yaml, codex_provider.py o claude_provider.py, recrea LiteLLM para evitar depender del estado importado por el proceso anterior:

docker compose --env-file .env up -d --force-recreate litellm

Verificación segura

Validar la configuración sin mostrar secretos:

docker compose --env-file .env config --quiet

Comprobar contenedores:

docker compose --env-file .env ps --all
docker compose --env-file .env logs --tail 100 litellm
docker compose --env-file .env exec litellm-db pg_isready

Comprobar autenticación de Codex en el host:

codex login status
stat -c '%a %U:%G %n' /home/felidae/.codex/auth.json

Comprobar autenticación de Claude en el host:

claude auth status
stat -c '%a %U:%G %n' /home/felidae/.claude/.credentials.json

Consultar el catálogo desde dentro del contenedor sin imprimir la clave:

docker compose --env-file .env exec litellm python -c '
import json, os, urllib.request
request = urllib.request.Request(
    "http://127.0.0.1:4000/v1/models",
    headers={"Authorization": "Bearer " + os.environ["LITELLM_MASTER_KEY"]},
)
with urllib.request.urlopen(request, timeout=15) as response:
    print([item["id"] for item in json.load(response)["data"]])
'

Consultar desde un cliente externo:

curl -sS https://llm.sherlockhomeless.net/v1/models \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

No uses set -x al trabajar con las claves.

Diagnóstico

401 antes de seleccionar un modelo

Normalmente indica que el consumidor no envió la LITELLM_MASTER_KEY correcta. Revisa la clave del cliente y que no contenga espacios o saltos de línea.

401 o error de autenticación sólo en modelos Codex

Comprueba:

  1. codex login status en el host.
  2. Que /home/felidae/.codex/auth.json exista y sea un archivo, no un directorio creado accidentalmente por Docker.
  3. Que tenga permisos 0600 y sea legible/escribible por el proceso esperado.
  4. Que el montaje aparezca en docker inspect litellm.
  5. Que el contenedor vea CODEX_AUTH_FILE=/root/.codex/auth.json.
  6. Los logs del contenedor, sin copiar tokens a una incidencia.

Si el refresh token fue revocado, ejecuta codex login de nuevo en el host y recrea el contenedor si fuera necesario.

401 o error de autenticación sólo en modelos Claude

Comprueba:

  1. Que claude auth status muestre loggedIn: true.
  2. Que /home/felidae/.claude/.credentials.json exista y tenga permisos 0600.
  3. Que el montaje aparezca en docker inspect litellm.
  4. Que CLAUDE_AUTH_FILE sea /root/.claude/.credentials.json.
  5. Que el access token y el refresh token estén presentes, sin imprimirlos.

Si están vacíos o revocados, ejecuta claude auth login en el host. El mismo archivo es compartido por Claude Code y LiteLLM, por lo que el contenedor verá la sesión nueva sin copiar secretos al repositorio.

429 en modelos Claude

Primero ejecuta una petición mínima con Claude Code. Si el CLI también devuelve 429, la suscripción puede haber agotado temporalmente su cuota: espera a la renovación o usa una API key dedicada. Si el CLI funciona pero LiteLLM falla, revisa que el adaptador use ANTHROPIC_AUTH_TOKEN, no envíe x-api-key y conserve las cabeceras e identidad de Claude Code descritas arriba. Los reintentos permanecen desactivados para no multiplicar consumo.

Claude rechaza temperature

claude_provider.py elimina este parámetro antes de delegar en Anthropic. Si otro parámetro queda obsoleto en modelos futuros, añádelo al filtrado sólo después de confirmar el error devuelto por la API.

Los modelos del YAML no coinciden con /v1/models

Revisa el estado persistido en PostgreSQL. Con STORE_MODEL_IN_DB=True, los modelos guardados en la base de datos pueden ampliar o modificar el catálogo efectivo.

PostgreSQL arranca después que LiteLLM

depends_on no verifica disponibilidad. Revisa pg_isready y los logs. Si el problema se repite, añade un healthcheck a PostgreSQL y una dependencia con condición de servicio saludable.

Un cliente acepta texto pero fallan herramientas

El proveedor Codex personalizado no reenvía actualmente todas las propiedades de Chat Completions o Responses. Verifica el cuerpo construido en _build_body() antes de atribuir el fallo a LiteLLM o al modelo.

Seguridad y endurecimiento recomendado

Prioridad alta:

  1. Tratar LITELLM_MASTER_KEY como una credencial con acceso indirecto a todos los proveedores y rotarla si se comparte fuera de los consumidores autorizados.
  2. Restringir en Traefik /key y /model si no necesitan exposición pública.
  3. Cambiar los permisos de .env a 0600.
  4. Añadir bloqueo y escritura atómica a la renovación de auth.json.
  5. Evaluar una API key dedicada de OpenAI para automatización o uso multiusuario.
  6. Sustituir el OAuth Claude.ai experimental por una ANTHROPIC_API_KEY, Bedrock o Vertex AI para cargas multiusuario o críticas.

Prioridad operativa:

  1. Añadir healthchecks a LiteLLM y PostgreSQL.
  2. Documentar o eliminar de PostgreSQL los modelos que no estén en config.yaml.
  3. Probar explícitamente streaming, llamadas no streaming y errores de renovación tras cada actualización de la imagen.
  4. Revisar periódicamente que únicamente LiteLLM monte auth.json y .credentials.json.

Backups y recuperación

  • Respaldar LITELLM_DB_DATA_PATH para conservar configuración y estado.
  • Mantener estable LITELLM_SALT_KEY; perderla puede impedir descifrar datos persistidos.
  • Si se respalda auth.json, hacerlo cifrado y con acceso muy restringido.
  • Aplicar la misma protección a .claude/.credentials.json.
  • No restaurar simultáneamente copias distintas de auth.json desde varios hosts: la rotación del refresh token puede invalidar copias anteriores.
  • Después de una restauración, validar PostgreSQL, /v1/models y un alias de cada proveedor por separado.

Estado observado el 16 de agosto de 2026

Esta sección es una fotografía, no una garantía permanente:

  • Compose válido usando .env.
  • LiteLLM y PostgreSQL activos desde hacía 13 días.
  • LiteLLM versión 1.82.6.
  • /health/liveliness respondía HTTP 200.
  • /v1/models respondía HTTP 200 y, tras limitar el catálogo, publica un único modelo Claude OAuth: claude-sonnet-5.
  • PostgreSQL aceptaba conexiones.
  • Codex CLI indicaba una sesión de ChatGPT activa.
  • Claude Code 2.1.233 indicaba loggedIn: true, proveedor first-party y suscripción Pro.
  • La consulta OAuth a GET /v1/models de Anthropic respondía HTTP 200.
  • Las generaciones normal y streaming mediante LiteLLM respondieron HTTP 200 después de adoptar el esquema Bearer y la identidad requerida por Claude Code.
  • La renovación de auth.json había ocurrido correctamente.
  • auth.json tenía permisos 0600.
  • .env tenía permisos 0664, pendiente de endurecimiento.
  • No había healthchecks Docker configurados.

Repite las verificaciones de este documento antes de usar esta fotografía para una intervención futura.