Presentamos OctoHub: una sola puerta de entrada para cada LLM que llaman tus agentes

Tu agente acaba de ejecutar una tarea. Hizo cuarenta llamadas a modelos. Algunas fueron a una máquina local con Ollama, otras a Anthropic, y una a OpenAI porque el modelo local se atragantó con un contexto largo. La tarea terminó. Ahora respóndeme tres preguntas: qué envió exactamente, cuánto costó y qué upstream respondió de verdad.

Si eras como nosotros, la respuesta honesta es "tendría que añadir prints y volver a ejecutarlo". Los paneles de los proveedores te muestran su porción. Los logs de tu agente te muestran su porción. Nadie te muestra la petición completa tal como cruzó el cable, etiquetada con qué clave la emitió y qué proveedor la atendió. La vista más útil — cada petición a través de un solo panel — es justo la que nadie te da, porque no hay un único lugar por el que pasen las peticiones.

OctoHub es ese único lugar. Es un proxy de LLM autoalojado que pones delante de tus agentes. Ellos hablan con un solo endpoint; OctoHub habla con quien tú le digas, y anota todo lo que ocurre en medio. Hoy lo liberamos como código abierto.


Qué es, en un diagrama

            ┌──────────────────┐
 agent   →  │  OctoHub proxy   │  →  openai
            │  (Rust / hyper)  │  →  anthropic
            │                  │  →  ollama (your GPU)
            └──────────────────┘  →  openrouter
                     │
                     └─→ your DB (SQLite / MySQL / PostgreSQL)
                         (api_keys, completions, embeddings)

Un único binario de Rust construido sobre hyper. Los clientes golpean un solo endpoint HTTP. El proxy resuelve el nombre del modelo, elige un upstream, reenvía la llamada a través de octolib — nuestra biblioteca cliente de LLM, la misma que usa cada herramienta de Muvon para hablar con los modelos — y persiste la petición, la respuesta, los recuentos de tokens y la latencia. El proxy en sí no guarda estado entre peticiones. El estado vive en dos lugares: octohub.toml para la configuración, y una base de datos para claves, logs y uso.

Deliberadamente no es varias cosas. No es una interfaz de chat — apunta tu aplicación hacia él. No es un enrutador semántico que elige el "mejor" modelo para un prompt; la elección del modelo es tarea de quien llama. No es un almacén de vectores; los embeddings pasan a través y se registran, pero OctoHub no los indexa. Añade autenticación, registro, balanceo de carga y una interfaz estable. No intenta ser listo con lo que devuelven los proveedores — cada campo relevante vuelve al cliente tal cual.


Por qué lo construimos

No nos propusimos construir un proxy. Estábamos ejecutando Octomind, nuestro runtime de agentes, contra una mezcla de modelos — una flota autoalojada en nuestras propias GPU para el trabajo barato de alto volumen, APIs de frontera para lo difícil. El montaje funcionaba. Lo que no funcionaba era verlo.

En el momento en que tienes más de un modelo detrás de una aplicación, pierdes el panel único. Cada proveedor tiene su propio dashboard, su propia contabilidad de tokens, su propia idea de qué es una "petición". Tu agente tiene sus propios logs, que te dicen lo que creyó que envió. Cuando una ejecución cuesta más de lo esperado, o un modelo empieza a devolver basura, estás cosiendo tres historias incompletas y adivinando en las costuras.

Escribimos más sobre ese dolor concreto — querer ver lo que un agente hizo de verdad, no lo que afirmó — en la observabilidad que queríamos. OctoHub es la respuesta de infraestructura a eso. Pon un proxy en el camino y el panel único existe por construcción: hay literalmente un solo lugar por el que pasa cada petición, así que hay un solo lugar donde registrarla.

La segunda razón fue el balanceo de carga. Ya estábamos ejecutando un agente entre muchos modelos a mano — cambios de config, variables de entorno, un cambio de nombre de modelo por ejecución. Queríamos un alias de modelo que significara "cualquiera de estos upstreams, tú eliges", y un límite de concurrencia para que una ráfaga de llamadas del agente no fundiera la máquina GPU local. Ambas cosas pertenecen a un proxy, no esparcidas por cada cliente.


Cómo fluye una petición a través de él

Toma un POST /v1/completions. Esto es lo que OctoHub hace con él:

  1. Autenticar. El token Authorization: Bearer es una clave de cliente de la tabla api_keys. OctoHub la busca, comprueba que está activa y etiqueta la petición con el ID de la clave. (Si arrancas el servidor sin clave maestra, solo se deshabilita la API de administración — se imprime una advertencia al arrancar; las llamadas de completions y embeddings siguen exigiendo una clave de cliente válida.)
  2. Comprobar la lista de permitidos. Las claves de cliente pueden restringirse a un conjunto de modelos. Una clave acotada a ["gpt", "anthropic:claude-haiku-4-5"] que pida cualquier otra cosa recibe un 403.
  3. Resolver el modelo. Si el campo model es un alias de [models], OctoHub lo expande a una cadena provider:model. Si ya es un provider:model simple, pasa directo. Los alias que mapean a una lista empiezan desde una entrada aleatoria y toman el primer proveedor que pueda admitir — eso es el balanceo de carga.
  4. Adquirir un permiso de proveedor. Si has fijado un límite de concurrencia para ese proveedor, la petición espera a una ranura libre. Sin ranura, sin 429 — la conexión HTTP simplemente queda abierta hasta que una se libere — hasta el timeout de cola (60 s por defecto), tras el cual recibes un 503. Throttling intencionado, registrado como tiempo de espera en cola.
  5. Llamar al upstream a través de octolib, con un plazo de operación configurable.
  6. Persistir y responder. La petición completa, la respuesta, los recuentos de tokens, el coste, el proveedor resuelto y la latencia van a la tabla completions. El cliente recibe la respuesta del upstream tal cual, más una cabecera X-Request-Id.

Ese X-Request-Id es la clave de unión. Es o bien un valor que pasaste tú (validado, devuelto) o un ULID nuevo. Aparece como req_id en cada línea de log de esa petición. Toma una respuesta de error, agarra la cabecera, haz grep en tus logs — tienes la historia completa.


Alias de modelos y balanceo de carga

La sección [models] es donde un nombre se abre en abanico hacia muchos upstreams:

[models]
# Un alias, un upstream
"sonnet"  = ["anthropic:claude-sonnet-5"]

# Un alias, varios upstreams — OctoHub elige uno al azar por petición
"workhorse" = ["ollama:kimi-k2.6", "ollama:minimax-m3", "openrouter:google/gemini-3.1-pro-preview"]

[embedding_models]
"voyage" = ["voyage:voyage-4"]

Un cliente que pide workhorse obtiene uno de los tres — OctoHub empieza desde una entrada aleatoria y toma el primer proveedor cuyas ventanas de tasa admitan la petición. Ese es todo el modelo de balanceo de carga — simple, predecible y justo lo suficiente para repartir el tráfico de agentes por una flota o por claves sin un enrutador aparte. Hoy no hay enrutamiento ponderado; los proveedores en cooldown por errores se depriorizan detrás de los sanos, y las peticiones que continúan una cadena se quedan con el proveedor que sirvió el turno anterior. Preferimos enviar la versión honesta de eso que insinuar un planificador más listo que el que existe.

Los clientes también pueden saltarse los alias por completo y enviar un provider:model crudo como openai:gpt-5.5. La tabla de alias es una comodidad, no una barrera — la barrera es la lista de permitidos por clave.


El modelo auto: di por qué, no cuál

Desde la 0.6.0 hay una segunda forma de elegir modelo, y es la que más usan nuestros propios agentes. En lugar de nombrar un modelo, el cliente envía "model": "auto" más una cabecera X-Model-Purpose — cualquier cadena que quieras — y OctoHub resuelve el propósito a un alias:

[auto]
default = "workhorse"
compression = "cheap"
supervisor = "sonnet"

Los propósitos son jerárquicos, divididos por -: supervisor-gate cae a supervisor, luego a default. Una sola fila supervisor cubre cada propósito supervisor-* hasta que fijes uno específico — defines exactamente tantas filas como opiniones tengas. Un propósito ausente o mal escrito degrada a default, nunca falla.

¿Para qué molestarse? Porque quien llama suele saber qué tipo de llamada está haciendo — un pase de compresión, una comprobación de gate, el bucle principal — y el operador sabe qué nivel merece ese tipo de llamada. El enrutamiento por propósito pone esa decisión en la config del proxy (o en un mapa de overrides por owner vía PUT /v1/admin/owners/:owner/auto, que gana al suelo de la config por completo) en lugar de clavar nombres de modelo en el agente. Octomind envía main, compression y la familia supervisor-* de serie.


Concurrencia por proveedor

Tu API de frontera puede aceptar treinta y dos peticiones en paralelo sin pestañear. Tu única máquina GPU corriendo Ollama no puede. Así que OctoHub limita las peticiones en vuelo por proveedor:

[providers.ollama]
concurrency = 5

[providers.openai]
concurrency = 32

Las peticiones que superan el límite se encolan dentro del proceso de OctoHub — la conexión del cliente se bloquea hasta que se abre una ranura. Los proveedores que no listes corren sin límite. El limitador es local al proceso y cuenta completions y embeddings juntos, ya que ambos fluyen por la misma conexión upstream. Es un semáforo por proveedor, nada exótico, pero significa que un agente que dispara cuarenta llamadas no tumbará el servidor de modelos sobre el que aterrizan.

La concurrencia no es el único mando. Cada proveedor también acepta límites de tasa de ventana fija — requests_per_minute, tokens_per_minute, requests_per_day, tokens_per_day — y un alias multi-upstream rota al siguiente candidato cuando una ventana se llena. Esas son las "ventanas de tasa" que comprueba el selector de alias: que un proveedor alcance su presupuesto diario de tokens no falla la petición, solo desplaza el tráfico al siguiente upstream que aún tenga margen.


La observabilidad que realmente obtienes

Esta es la parte para la que construimos OctoHub, así que tiene dos superficies.

Logs estructurados en stdout — bonitos si estás en un TTY, JSON si no. Cada petición completada emite una línea:

{
	"level": "INFO",
	"message": "request completed",
	"req_id": "01HMQGSB3R",
	"route": "/v1/completions",
	"status": 200,
	"dur_ms": 1523,
	"api_key_id": 1,
	"model": "workhorse",
	"provider": "ollama",
	"queued_ms": 0,
	"tok_in": 56,
	"tok_out": 120
}

Ves la clave que la emitió, el nombre del modelo que pidió el cliente, el proveedor que de verdad respondió, cuánto encoló, cuánto tardó, y los tokens de entrada y salida. El campo provider es la respuesta a "sobre qué upstream cayó la elección aleatoria" — sin volver a ejecutar nada.

Métricas de Prometheus en un puerto aparte (127.0.0.1:9090 por defecto, GET /metrics). Todo lleva el prefijo octohub_:

Métrica Qué te dice
octohub_requests_total volumen de peticiones por route, method, status
octohub_request_duration_seconds histograma de latencia de extremo a extremo
octohub_completions_total volumen de completions por model, provider, status
octohub_completion_tokens_total tokens de entrada/salida por model y provider
octohub_provider_queue_wait_seconds tiempo esperando un permiso de concurrencia
octohub_provider_in_flight peticiones activas en cada provider ahora mismo

El histograma de espera en cola es el indicador adelantado de saturación — cuando el P99 sube, tu flota es el cuello de botella antes de que ninguna petición caduque. Un par de líneas de PromQL te dan la tasa de error por modelo y los tokens de salida por segundo por proveedor. Activa per_key = true y las métricas de completions ganan una etiqueta api_key_id, para que puedas facturar o atribuir coste por cliente. (Cuidado con la cardinalidad si emites miles de claves.)

Para el registro completo — no agregados, los bytes reales del prompt y la respuesta — la API de administración sirve el historial crudo de completions directo desde la base de datos.


Claves multi-tenant y la API de administración

OctoHub tiene dos capas de autenticación. La clave maestra (fijada en octohub.toml) protege la API de administración. Las claves de cliente — emitidas a través de esa API de administración, almacenadas en la base de datos — autentican los endpoints de completions y embeddings. Cada completion se etiqueta con la clave emisora, que es lo que hace que el seguimiento de uso por tenant funcione.

Hay un envoltorio shell, octohub-admin.sh, para la operación diaria:

export OCTOHUB_MASTER_KEY=your-master-secret

# Emitir una clave de cliente, restringida a dos modelos
./octohub-admin.sh keys create ci-pipeline --allowed-models gpt,anthropic:claude-haiku-4-5

# Uso diario para las claves 1 y 2
./octohub-admin.sh usage --bucket day --key 1,2

# Sacar las últimas 20 completions crudas — entrada y salida completas
./octohub-admin.sh completions --limit 20

Las claves se revocan, nunca se borran — los registros de uso están enlazados al ID de la clave, así que el historial sobrevive. El uso se acumula por hour, day, week o month, filtrable por clave y rango de tiempo. Los mismos datos están disponibles como HTTP plano si prefieres no usar el script.

Dos superficies operativas más que conviene conocer. GET /v1/admin/status informa de la salud por modelo observada del tráfico real — no una sonda sintética, así que un solo tropiezo no pinta una luz roja en tu dashboard. Y enviar al proceso un SIGHUP recarga octohub.toml en caliente — nuevos alias, nuevos límites, sin reinicio.


Ponlo en marcha en 5 minutos

OctoHub es un único binario. Constrúyelo, escribe una config, arráncalo, emite una clave.

# 1. Compilar desde fuente
git clone https://github.com/Muvon/octohub
cd octohub && cargo build --release

Escribe un octohub.toml mínimo:

[server]
host = "127.0.0.1"
port = 8080
api_key = "your-master-secret"        # habilita auth + la API de administración
db_url = "sqlite://octohub.db"         # esquema autocreado en el primer arranque

[models]
"workhorse" = ["ollama:kimi-k2.6", "openrouter:google/gemini-3.1-pro-preview"]

[metrics]
enabled = true
bind = "127.0.0.1:9090"

[providers.ollama]
concurrency = 5

Arráncalo y crea una clave de cliente:

# 2. Arrancar el servidor (apunta a la config con -c si no está en el cwd)
./target/release/octohub

# 3. Emitir una clave de cliente
curl -X POST http://127.0.0.1:8080/v1/admin/keys \
  -H "Authorization: Bearer your-master-secret" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app"}'
# → {"id": 1, "key": "abc...xyz", ...}   ← guárdala, se muestra una sola vez

# 4. Hacer un completion a través del proxy
curl -X POST http://127.0.0.1:8080/v1/completions \
  -H "Authorization: Bearer abc...xyz" \
  -H "Content-Type: application/json" \
  -d '{"model": "workhorse", "input": "Explain Rust in one sentence."}'

La base de datos arranca como SQLite — sin instalación. Apunta db_url a MySQL o PostgreSQL cuando lo superes; el esquema se crea automáticamente en la primera conexión. Las claves de API de los proveedores (OPENAI_API_KEY, ANTHROPIC_API_KEY y compañía) viven en el entorno, leídas por octolib exactamente como los proveedores esperan.

OctoHub también habla el clásico OpenAI Chat Completions en POST /v1/chat/completions, así que cualquier SDK o herramienta compatible con OpenAI apunta a él como una base URL de reemplazo directo. (Una salvedad honesta: el streaming no está implementado — una petición con "stream": true recibe un 501.)


Apuntar Octomind hacia él

OctoHub y Octomind fueron construidos el uno para el otro, y octolib trae un proveedor nativo octohub: — sin capa de compatibilidad con OpenAI, habla la Responses API de OctoHub directamente. Dos variables de entorno los conectan:

export OCTOHUB_API_URL=http://127.0.0.1:8080   # tu servidor OctoHub
export OCTOHUB_API_KEY=abc...xyz               # una clave de cliente que emitiste

Ahora cualquier referencia de modelo de Octomind de la forma octohub:<alias> se enruta a través del proxy:

octomind run --model octohub:workhorse developer:general

El agente cree que habla con un solo proveedor. Tras el proxy, workhorse se abre en abanico por tu flota, cada llamada se registra con el coste y el upstream que respondió, y el límite de concurrencia mantiene en pie la máquina GPU. El agente sigue siendo simple; la visibilidad vive donde las peticiones de verdad cruzan el cable.

Y esto no es un montaje de laboratorio. Octomind Cloud — nuestro runtime de agentes gestionado — ejecuta cada agente de cliente a través de OctoHub en producción. Las llamadas llegan etiquetadas con propósitos (main, compression, supervisor-gate), el modelo auto enruta cada una al nivel correcto, y cada petición aterriza en el registro con un coste y el upstream que respondió. El mismo binario que puedes clonar es el que está delante de nuestra flota.


Código abierto, Rust, Apache-2.0

OctoHub está en GitHub bajo Apache-2.0. Es un único binario de Rust basado en hyper — pequeño, rápido y ligero en dependencias. SQLite por defecto, así que no hay nada que levantar, MySQL y PostgreSQL cuando los necesites. La configuración es un fichero TOML más un puñado de overrides de entorno OCTOHUB_* para las cosas que ajustas por despliegue (OCTOHUB_DB_URL, OCTOHUB_LOG_FORMAT, OCTOHUB_METRICS_BIND).

Es temprano — versión 0.6.4, el número honesto. Hace lo que dice: una puerta de entrada, alias de modelos, enrutamiento auto por propósito, balanceo de carga con failover y cooldown, concurrencia y límites de tasa por proveedor, comprobaciones de modalidad que saltan a los proveedores que no pueden con tus imágenes o vídeo, claves multi-tenant, registro completo de peticiones y un endpoint de Prometheus. El soporte de proveedores sigue creciendo — Google Studio llegó en la 0.6.2 junto a los más de veinte existentes. Aún no hace streaming ni enrutamiento ponderado, y prefiero decírtelo a que lo descubras en producción.

Si ejecutas más de un modelo detrás de una aplicación — y si construyes agentes, lo haces — OctoHub te da el panel único que el resto del stack asume calladamente que existe. Clónalo, apunta un agente hacia él, y mira pasar una ejecución con el coste adjunto.

— Don

OctoHub es código abierto bajo Apache-2.0, desarrollado por Muvon Un Limited. Consíguelo en GitHub — issues y pull requests bienvenidos.