docs(litellm): document deployment and Codex auth

This commit is contained in:
Eduardo David Paredes Vara
2026-08-16 20:02:23 +00:00
parent ac82477aee
commit 4ab365deda
+407 -51
View File
@@ -1,105 +1,461 @@
# LiteLLM
LiteLLM es la pasarela central de modelos del repositorio. Ofrece una API compatible con OpenAI a aplicaciones como Open WebUI, n8n y Paperless AI, mientras mantiene las credenciales reales de los proveedores en un único servicio.
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.
## Qué despliega
Este servicio se administra directamente con Docker Compose. No depende de
Portainer y no debe desplegarse usando `stack.env`.
- `litellm`: proxy, catálogo de modelos y proveedor personalizado de Codex.
- `litellm-db`: PostgreSQL para configuración y estado persistente de LiteLLM.
- `config.yaml`: alias públicos y proveedor asociado a cada modelo.
- `codex_provider.py`: adaptación entre la API de LiteLLM y la API Responses de Codex.
## Arquitectura
Esta separación evita configurar una credencial de OpenAI o Codex distinta en cada consumidor. Las aplicaciones sólo conocen la URL de LiteLLM, una clave de acceso de LiteLLM y el alias del modelo.
```text
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
|
+----> PostgreSQL 16
```
## Redes y exposición
Los consumidores nunca reciben una credencial de DeepSeek ni los tokens de
Codex. Sólo conocen la URL de LiteLLM, una clave de LiteLLM y un alias de
modelo.
LiteLLM pertenece a su red interna `litellm` y a la red externa `proxy`. PostgreSQL sólo está en la red interna. Los consumidores conectados a `proxy` pueden usar:
## 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 del proveedor Codex. |
| `codex_provider.py` | Adaptador entre LiteLLM y la API Responses usada por Codex. |
| `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:
```text
./config.yaml -> /app/config.yaml (solo lectura)
./codex_provider.py -> /app/codex_provider.py (solo lectura)
/home/felidae/.codex/auth.json -> /root/.codex/auth.json (lectura/escritura)
```
El montaje de `auth.json` es deliberadamente escribible porque el proveedor
renueva el token y persiste la sesión actualizada. Esto también significa que
el contenedor tiene acceso al refresh token de la cuenta de ChatGPT.
### `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:
```bash
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:
```text
http://litellm:4000/v1
```
El acceso público y la interfaz se enrutan mediante la configuración dinámica de Traefik en `/opt/traefik/dynamic/litellm.yml`. La API debe exigir `LITELLM_MASTER_KEY`; la protección SSO de la interfaz no sustituye esa clave.
El dominio público es:
## Configuración
```text
https://llm.sherlockhomeless.net
```
El `.env` local debe definir, como mínimo:
El enrutamiento vive fuera de esta carpeta, en
`/opt/traefik/dynamic/litellm.yml`:
| Variable | Función | Motivo |
| --- | --- | --- |
| `LITELLM_MASTER_KEY` | Autentica a los consumidores de la API | Impide usar los modelos sólo por alcanzar la red |
| `LITELLM_SALT_KEY` | Cifra datos sensibles almacenados por LiteLLM | Debe permanecer estable tras el primer arranque |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Base de datos interna | No se exponen fuera de la red `litellm` |
| `LITELLM_DB_DATA_PATH` | Persistencia de PostgreSQL | Permite recrear contenedores sin perder estado |
| `DEEPSEEK_API_KEY` | Acceso directo a DeepSeek | Sólo se necesita para los alias `thsllm-*` |
- 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.
`config.yaml` publica alias estables. Los consumidores deben pedir el alias, no el identificador interno del proveedor. Así se puede cambiar el backend sin reconfigurar cada aplicación.
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`
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.
### 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
El Compose monta:
### 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:
```text
/home/felidae/.codex/auth.json -> /root/.codex/auth.json
/home/felidae/.codex/auth.json
```
La variable `CODEX_AUTH_FILE` apunta a la ruta interior. El montaje es de lectura y escritura porque `codex_provider.py` renueva el access token mediante el refresh token y guarda el resultado en el mismo archivo.
Flujo:
1. El usuario inicia sesión con Codex en el host y genera `~/.codex/auth.json`.
2. Docker monta ese archivo únicamente dentro de LiteLLM.
3. El proveedor lee `access_token`, `refresh_token` y `account_id`.
4. Si el JWT vence en menos de 60 segundos, solicita un token nuevo y actualiza el archivo montado.
5. LiteLLM envía la petición a Codex y devuelve una respuesta compatible con OpenAI al consumidor.
El archivo contiene credenciales reutilizables. No debe copiarse al repositorio, incluirse en imágenes, logs, backups sin cifrar ni montarse en Paperless, n8n u Open WebUI. `.gitignore` excluye `.codex`, pero conviene comprobar antes de cada commit:
Codex CLI informa del método activo con:
```bash
git status --ignored --short | grep codex
codex login status
```
El usuario que ejecuta Docker debe poder leer y escribir el archivo. Si el host o usuario cambia, actualiza la ruta del volumen en `docker-compose.yml`; una ruta inexistente puede terminar creada como directorio y provocar errores de autenticación.
El archivo contiene material reutilizable, incluidos access token, refresh
token y el identificador de cuenta. Debe tratarse como una contraseña:
## Modelos Codex
- 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.
Los alias `gpt-*` configurados con `custom_llm_provider: codex` pasan por `codex_provider.py`. Internamente se usa el prefijo `cx-` para evitar colisiones con los modelos OpenAI nativos de LiteLLM; el proveedor vuelve a convertirlo antes de llamar a Codex.
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](https://learn.chatgpt.com/docs/auth).
El proveedor admite llamadas síncronas, asíncronas y streaming. Codex responde siempre mediante streaming; para una llamada no streaming, el proveedor agrega los fragmentos antes de responder.
### Flujo de una petición
## Configurar consumidores
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.
Un consumidor compatible con OpenAI necesita:
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.
## 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:
```text
Base URL: http://litellm:4000/v1
API key: valor de LITELLM_MASTER_KEY
Model: gpt-5.6-sol
Model: uno de los alias publicados
```
Usa la URL interna cuando ambos contenedores compartan `proxy`. Usa la URL HTTPS sólo para clientes externos. Nunca uses el token de Codex como API key del consumidor.
Usa la URL interna cuando ambos contenedores compartan `proxy`. Para clientes
externos usa HTTPS. Nunca configures el token de Codex como API key de un
consumidor.
Flujo de Paperless AI:
```text
Paperless -> Paperless AI -> LiteLLM -> proveedor Codex -> Codex
```
Open WebUI y n8n siguen el mismo patrón desde la red `proxy`.
## Despliegue
Desde esta carpeta:
```bash
cd litellm
docker compose --env-file .env config
docker compose --env-file .env up -d
docker compose --env-file .env ps
```
## Verificación
No usar:
```bash
docker compose --env-file .env ps
docker compose --env-file .env logs -f litellm
curl -sS http://127.0.0.1:4000/v1/models \
docker compose --env-file stack.env up -d
```
Tras modificar `config.yaml` o `codex_provider.py`, recrea LiteLLM para evitar
depender del estado importado por el proceso anterior:
```bash
docker compose --env-file .env up -d --force-recreate litellm
```
## Verificación segura
Validar la configuración sin mostrar secretos:
```bash
docker compose --env-file .env config --quiet
```
Comprobar contenedores:
```bash
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:
```bash
codex login status
stat -c '%a %U:%G %n' /home/felidae/.codex/auth.json
```
Consultar el catálogo desde dentro del contenedor sin imprimir la clave:
```bash
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:
```bash
curl -sS https://llm.sherlockhomeless.net/v1/models \
-H "Authorization: Bearer $LITELLM_MASTER_KEY"
```
Si no se publica el puerto en el host, ejecuta la prueba desde un contenedor unido a `proxy` o usa el dominio HTTPS.
No uses `set -x` al trabajar con las claves.
Errores `401` que mencionen Codex suelen indicar que `auth.json` falta, no tiene permisos o ya no puede renovarse. Errores `401` devueltos antes de seleccionar modelo suelen indicar una `LITELLM_MASTER_KEY` incorrecta.
## Diagnóstico
## Persistencia y copias de seguridad
### `401` antes de seleccionar un modelo
Respalda la ruta `LITELLM_DB_DATA_PATH`. Trata `auth.json` como un secreto independiente: si se respalda, debe hacerse cifrado y con acceso restringido. No restaures simultáneamente versiones distintas del archivo desde varios hosts, porque la rotación del refresh token puede invalidar la copia anterior.
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.
### 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.
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`.
## 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.
- 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 publicaba diez modelos.
- PostgreSQL aceptaba conexiones.
- Codex CLI indicaba una sesión de ChatGPT activa.
- 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.