Un único endpoint para todos tus agentes: el gateway que esconde qué modelo hay detrás
OpenCode, Hermes, Pi, DeepSeek Harness y Codex hablan con una sola dirección y un solo modelo virtual, gx10-local. Detrás rotamos DeepSeek, Qwen, GLM o MiMo sin tocar ningún cliente. Qué hace el gateway y qué hace Mission Control.
En el laboratorio conviven media docena de clientes: Hermes, OpenCode, OpenChamber, Pi, DeepSeek Harness y, a veces, Codex. Y cada pocos días cambiamos el modelo que corre en las GX10. Si cada cliente apuntara al puerto, al nombre y a la clave de cada modelo, una rotación significaría editar seis configuraciones. Y la primera vez que lo hicimos así, descubrimos que una rotación no cambia un campo del cliente: cambia cuatro .
La solución es poner una sola puerta delante de todo.
La idea: un modelo virtual que nunca cambia de nombre
Todos los clientes usan:
base URL: http://127.0.0.1:8099/v1 (o el relay, desde fuera de la GX10)
modelo: gx10-local
gx10-local no existe. Es un alias. El gateway mira qué modelo está residente y sano en
ese momento y reenvía la petición allí, sustituyendo el nombre por el real.
OpenCode ─┐
Hermes ─┤ ┌─► DeepSeek (llama.cpp / DS4)
Pi ─┼─► gateway :8099 ────────┼─► Qwen (SGLang)
DSH ─┤ modelo «gx10-local» ├─► GLM-5.3 (vLLM, en las dos GX10)
Codex ─┘ └─► MiMo (SGLang, staging)
/v1/models anuncia siempre gx10-local, con el contexto y la salida máxima del modelo que
haya detrás, para que los clientes ajusten sus límites solos.
Lo que hace el gateway (y lo que no)
Hace:
- Falla cerrado. Si no hay residente, hay dos, está cargando o no tiene credencial, responde 503 explícito. Nunca «prueba suerte» con un backend a medio arrancar.
- Guarda las claves. Cada modelo tiene su clave; el gateway tiene la suya. Los clientes
solo conocen la del gateway. Las claves viven en un fichero
0600fuera de Git y nunca se imprimen: para rotarlas hay scripts que lo hacen de forma transaccional. - Inyecta un prompt de sistema por ruta cuando un modelo lo necesita. Un ejemplo real: un modelo resistía 0 de 5 ataques de inyección sin ayuda y 5 de 5 con un prompt defensivo añadido por el gateway, conservando además el prompt de sistema del cliente.
- Pone un suelo a la salida en los modelos que razonan. Qwen piensa por defecto: con un
max_tokensde 30 gasta todo el presupuesto pensando y devuelve contenido vacío. Subir la capacidad anunciada no basta porque manda elmax_tokensque envía el cliente. El gateway eleva los valores cortos a un mínimo (2048) en esas rutas, y nunca baja los largos. - Drena antes de rotar. Durante una rotación, las peticiones nuevas reciben
503 DRAININGy el controlador espera a que terminen las que están en vuelo.
No hace: reparar streaming ni reinterpretar tool calls. Lo que devuelve el modelo llega tal cual al cliente. Si un modelo emite mal las herramientas, se ve.
Y se ejecuta con el mínimo de privilegios: servicio de usuario con NoNewPrivileges,
ProtectSystem=strict y ProtectHome=read-only; escucha solo en 127.0.0.1.
Mission Control: quién decide qué modelo está residente
El gateway enruta; Mission Control decide. Es un panel web propio en el que cada modelo es
un «cartucho» con su memoria, contexto, runtime y estado (hot, loading, degraded,
cold). Desde el móvil puedes pedir «pon GLM» y el controlador ejecuta una transacción:
- drenar el gateway;
- parar el residente actual;
- arrancar el nuevo y esperar a su
/health; - actualizar los clientes que lo necesiten;
- reanudar y hacer un smoke test real (una llamada con herramientas);
- confirmar… o, si algo falla, volver al modelo anterior y repetir clientes y smoke.
Además muestra memoria unificada, swap, temperatura, potencia y un banco de velocidad reproducible (DYNO) contra el alias estable.
Los dos nodos, simétricos
Cada GX10 tiene su gateway y su relay. Los clientes eligen máquina, no modelo:
| modo normal | modo clúster | |
|---|---|---|
| entrada de GX10-1 | su residente | el modelo grande repartido en las dos |
| entrada de GX10-2 | su residente | ⛔ sin modelo: está dentro del clúster |
No es un fallo: no se pueden tener dos cerebros y a la vez juntar las dos máquinas en uno.
Trampas que nos encontramos montándolo
- Un
/healthprotegido parece «cargando» para siempre si el panel lo sondea sin credencial. - El panel sondeaba los modelos sin clave y generaba veinte errores por minuto en los logs .
- El contador de peticiones en vuelo se quedaba pegado en 1 sin tráfico. Justo la señal que usa el drenaje para decidir si puede rotar: puede hacerle esperar para siempre .
- Inyectar un prompt de sistema rompe a Qwen con un 400 si el cliente ya manda uno: quedan
dos mensajes
systemy el segundo ya no va al principio. Nemotron lo tolera; Qwen no . - Un health compartido no dice qué modelo hay detrás: pregunta al gateway, no al puerto.
- Cambiar la configuración de un cliente no basta: el proceso de larga vida la lleva en memoria hasta que lo reinicias (P-080).
Cómo empezar tú, en pequeño
No necesitas nada de esto para un solo modelo. Pero en cuanto tengas dos, un proxy mínimo que lea «qué está activo» de un fichero y reenvíe a ese puerto te ahorra editar clientes. LiteLLM u otros proxies OpenAI-compatibles lo hacen; el nuestro es propio porque queríamos el drain, el fallo cerrado y la integración con el panel.
curl -s http://127.0.0.1:8099/health
curl -s -H "Authorization: Bearer $CLAVE" http://127.0.0.1:8099/v1/models
Siguiente y último: el kit de diagnóstico para cuando algo no funciona desde fuera y no sabes por dónde empezar.