← Tu laboratorio en cualquier parteSerie Remoto · paso 10 de 11

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.

Dificultadavanzada
Tiempouna tarde
IngredientesNode.js · systemd de usuario · vLLM / SGLang / llama.cpp · un panel de control propio

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 0600 fuera 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_tokens de 30 gasta todo el presupuesto pensando y devuelve contenido vacío. Subir la capacidad anunciada no basta porque manda el max_tokens que 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 DRAINING y 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:

  1. drenar el gateway;
  2. parar el residente actual;
  3. arrancar el nuevo y esperar a su /health;
  4. actualizar los clientes que lo necesiten;
  5. reanudar y hacer un smoke test real (una llamada con herramientas);
  6. 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 /health protegido 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 system y 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.