Tu API key acabó en el host equivocado: así arregla Hermes el enrutado de base_url

Por fin consigues que te aprueben el relay interno. Una sola línea en ~/.hermes/config.yaml — model.base_url — apunta Hermes al gateway de tu empresa, la revisión de seguridad da el visto bueno y durante semanas todo parece ir bien: las respuestas llegan, las tools se ejecutan, la factura cae en la cuenta correcta. Hasta que alguien de redes hace una pregunta pequeña. ¿Por qué lleva tu credencial del relay pegada al tráfico que sale hacia api.vendor.example? Vuelves a mirar la configuración. Sigue diciendo tu relay. Y ese es justo el problema: en cuatro rutas de código, esa línea nunca se leyó.
Qué promete model.base_url
Cada provider de Hermes tiene un host canónico — https://api.x.ai/v1, el endpoint de OpenRouter, chatgpt.com/backend-api/codex. Un model.base_url configurado es el override: «no hables con el host canónico, habla con este». La gente lo configura por motivos que tienen que ver con el control:
- un relay propio o de empresa que registra logs, aplica rate limits y factura internamente;
- un endpoint regional, cuando los datos tienen que quedarse en una jurisdicción;
- un proxy delante de un proveedor, o un gateway compatible con OpenAI que reparte carga entre varios labs.
Cuando el override se descarta, la petición no falla con un error visible. Se completa — contra el host equivocado, llevando puesta la credencial que emitiste para el otro. Eso es lo que reparan dos arreglos mergeados el 2026-09-25.
Bug uno: cuatro rutas que nunca consultaban la configuración
PR #121614 agrupa cuatro bugs reportados por separado que comparten una misma forma: se descarta un base_url configurado y el código acaba en el host canónico del proveedor, enviando la petición y la API key a un sitio que el usuario no eligió.
- Resolución en runtime (xAI). El resolver de runtime tiene una tabla de atajos para providers «simples». La entrada de xAI en esa tabla llevaba una URL por defecto vacía, así que la regla que normalmente se aplica — si la fila del pool todavía tiene el valor por defecto del registro, usa
model.base_url— nunca se ejecutaba para xAI, y la fila sembrada desde el entorno conhttps://api.x.ai/v1ganaba siempre. - Entradas de fallback. Cuando un provider primario falla y Hermes activa un fallback, se reenvía el
base_urlde esa entrada. Dos providers con sus propias ramas de resolución (OpenRouter y la rama de Anthropic) lo tiraban a la basura, mientras que todos los demás lo respetaban. - Listado del selector de modelos. El listado que muestra
/modeliba directo a los fetchers canónicos de cada provider y nunca preguntaba si habías apuntado ese provider a un relay, así que el catálogo de tu relay era invisible. - Validación de
/model --provider. La ruta de validación tenía el mismo punto ciego.
Tres de los cuatro ya tenían arreglos de la comunidad, que se incorporaron a la consolidación manteniendo la autoría. La consecuencia práctica para quien corre un relay: el descubrimiento y la validación de modelos ahora sondean el endpoint que la inferencia usa de verdad, en lugar de un host del que te alejaste a propósito.
Bug dos: un gateway de Codex propio seguía llamando a chatgpt.com
El segundo arreglo, PR #121751, es el que se lee como un incidente de seguridad. Si ejecutas Codex a través de un gateway propio — HERMES_CODEX_BASE_URL, o un gateway declarado solo mediante model.base_url — varias rutas laterales tenían chatgpt.com/backend-api/codex escrito a fuego y luego adjuntaban a la petición la credencial resuelta (la del gateway).
La lista es más larga de lo que esperarías, porque casi toda ella es invisible en el uso normal:
- la sonda de longitud de contexto que se ejecuta en cada turno de chat;
- el listado de Codex del selector
/model; - el flujo Codex de
hermes model; - el cliente Codex del plugin de imágenes;
- el cliente Codex auxiliar;
- la sonda de cuota de credenciales del pool;
/usage.
Tres reportes llegaron la misma mañana (2026-09-24) — uno sobre las rutas de chat, otro sobre la de imágenes y otro consiguiendo que la sonda y el selector construyeran sus URLs de catálogo a partir de la base resuelta — y después un revisor señaló la fuga que quedaba: el selector y las rutas de imagen seguían reconstruyendo el destino a partir de variables de entorno del ambiente, así que un gateway declarado solo en model.base_url seguía filtrándose. Los usuarios de OAuth directo con ChatGPT nunca se vieron afectados; era un problema de gateways autoalojados.
El arreglo, en una frase
Credencial y destino se resuelven ahora como una pareja. El resolver informa a qué host enruta realmente una credencial del pool, usando una única regla de precedencia en todas partes — override del entorno, luego model.base_url, luego la URL de la propia fila del pool — y la barrera que «rechaza una clave que no sea JWT» ahora solo se aplica a las sondas dirigidas a chatgpt.com. Una clave de gateway puede consultar su propio gateway; ya no puede viajar al público.
Cómo comprobar tu propia configuración
No hace falta leer el código fuente para saber si te afectó. Lo que necesitas es saber a qué host llegó de verdad tu tráfico.
# ¿qué dice realmente tu configuración?
hermes config get model.base_url
# si ejecutas Codex a través de un gateway, ¿está puesto el override en esta shell?
echo "$HERMES_CODEX_BASE_URL"
# y ¿qué había en el entorno de esa shell cuando arrancó el agente?
grep -n "base_url" ~/.hermes/config.yaml
Después pásate al lado de los logs, porque la configuración no es egress:
- El access log de tu relay es la verdad absoluta. Si tu gateway registró el tráfico, la respuesta de auditoría es un grep. Si el host del proveedor aparece en los logs de una tool mientras tu relay no muestra nada, ibas por la ruta equivocada.
- Mira el tráfico de las sondas, no solo el de chat. La sonda de longitud de contexto se dispara en cada turno, lo que la convierte en la evidencia más ruidosa de una fuga — y en la más fácil de pasar por alto, porque nunca aparece en la transcripción.
- Vuelve a verificar después de actualizar. Los dos PR están solo en
mainahora mismo: se mergearon el 2026-09-25, un día después de que se cortara el tag v0.21.5, así que no están en ese tag (la página de la release v0.21.5 recoge lo que sí incluye). Llegan a las instalaciones con la siguiente release o con unhermes updateque sigamain. - Trata «ha respondido» como cero evidencia. Los dos bugs producían respuestas normales y bien formadas. La única comprobación fiable es la del lado del destino.
Si usas model.base_url por residencia de datos o por cumplimiento normativo, esto merece una reverificación explícita en lugar de darlo por hecho: apunta a un relay que registre logs, envía un mensaje y confirma por dónde se fueron los bytes. Quien corre un proxy delante de un lab — el montaje que cubre la guía del provider de suscripción a Claude — está exactamente en el grupo que estos dos arreglos protegen. Si además enrutas el descubrimiento de modelos o los hooks a través de un endpoint propio, el recorrido por los hooks de los provider plugins muestra hasta dónde se supone que llega ese override, y las notas sobre la caché de configuración explican por qué un valor cambiado puede tardar un arranque en verse.
Qué llevarte
Un endpoint propio no es un ajuste cosmético: es una declaración sobre dónde pueden ir tus datos y tus credenciales. El patrón que hay detrás de los dos arreglos es el mismo que conviene llevar a tu propia automatización: que una petición se complete no prueba que fuera donde pediste. Cada vez que una tool, un provider o un plugin acepta un override tipo base_url, la pregunta de auditoría no es «¿funcionó?» sino «¿qué host respondió y qué credencial vio?». En este proyecto la respuesta ahora se resuelve una sola vez, en un único sitio, con un orden de precedencia documentado — y la barrera que se niega a enviar una clave de gateway a un host público está acotada al host para el que se escribió.