Ir al contenido
OmniRoute source

open-sse Architecture (Español)

¿Por qué un paquete de workspace independiente?

Sección titulada «¿Por qué un paquete de workspace independiente?»

open-sse/ es un workspace independiente dentro del monorepo de OmniRoute por varias razones:

  1. Reutilización — open-sse se publica como @omniroute/open-sse en npm, por lo que otros proyectos pueden usarlo de forma independiente
  2. Límites claros — el motor de streaming está desacoplado de la capa de interfaz de usuario/base de datos específica de OmniRoute
  3. Rendimiento — el motor no tiene dependencias de Next.js, lo que permite arranques en frío más rápidos en contextos de CLI/serverless
  4. Versionado — open-sse puede publicar versiones siguiendo su propio calendario
package.json
"workspaces": ["open-sse"]

open-sse/
├── index.ts # Punto de entrada público
├── types.d.ts # Exportaciones públicas de tipos
├── package.json # @omniroute/open-sse
├── config/ # Configuraciones de proveedores, constantes y registros
├── executors/ # Ejecutores HTTP por proveedor (67 + base.ts/index.ts)
├── handlers/ # Controladores de solicitudes (chatCore, responses, etc.)
├── lib/ # Utilidades internas
├── mcp-server/ # Servidor Model Context Protocol
├── services/ # ~298 módulos de servicio
├── transformer/ # Transformador de formato de Responses API
├── translator/ # Traducción de formatos (OpenAI ↔ Claude ↔ Gemini)
└── utils/ # Utilidades compartidas (registros, errores, streaming, etc.)

| Directorio | Archivos | Propósito | | executors/ | 167 | Ejecutores HTTP por proveedor (unificados mediante la factoría DefaultExecutor) | | handlers/ | 157 | Puntos de entrada de solicitudes (chatCore, responses, embeddings) | | services/ | ~536 | Enrutamiento, almacenamiento en caché, limitación de frecuencia, actualización, etc. | | translator/ | 56 | Conversión de formatos (OpenAI ↔ Claude ↔ Gemini) | | mcp-server/ | 44 | Herramientas y transportes MCP | | utils/ | ~108 | Utilidades transversales (registros, errores, streaming) | | config/ | ~339 | Configuraciones de proveedores, constantes y registros |


Cada solicitud a un LLM atraviesa un flujo de 5 etapas:

┌──────────────┐
Solicitud HTTP │ 1. ENRUTAR │ resolución de combos, selección de modelo
(ruta de Next.js) └──────┬───────┘
│
▼
┌──────────────┐
│ 2. TRADUCIR │ conversión de formato (OpenAI ↔ Claude ↔ Gemini)
└──────┬───────┘
│
▼
┌──────────────┐
│ 3. EJECUTAR │ ejecutor del proveedor, HTTP, reintentos, disyuntor
└──────┬───────┘
│
▼
┌──────────────┐
│ 4. STREAMING│ transformación SSE, contrapresión
└──────┬───────┘
│
▼
┌──────────────┐
│ 5. REGISTRAR│ seguimiento de uso, registro de llamadas, clasificación de errores
└──────┬───────┘
│
▼
Respuesta HTTP (SSE o JSON)

Punto de entrada: handleComboChat() en services/combo.ts

Resuelve la solicitud en una tupla concreta (provider, model, account, credentials):

  • Busca el combo por ID (o crea un combo virtual para los modelos auto/*)
  • Aplica la estrategia de enrutamiento (prioridad, ponderada, round-robin, etc.)
  • Excluye los proveedores con problemas (disyuntor)
  • Selecciona el siguiente destino viable

Para los modelos auto/*, esta etapa también:

  • Ejecuta el algoritmo de puntuación de 16 factores (services/autoCombo/)
  • Selecciona un par provider+model según el estado, el coste, la latencia, etc.

Si el formato de origen (p. ej., OpenAI) difiere del formato de destino (p. ej., Claude), la solicitud se traduce:

  • Prompt del sistema → mensaje del sistema
  • Definiciones de herramientas → formato de herramientas específico del proveedor
  • Parámetros de razonamiento/pensamiento → equivalentes específicos del proveedor
  • Normalización del rol de los mensajes (developer → system para proveedores distintos de OpenAI)

translator/index.ts expone:

translateRequest(body, sourceFormat, targetFormat): TranslatedRequest
needsTranslation(source, target): boolean

Punto de entrada: getExecutor(providerId).execute(request, options)

Todos los proveedores utilizan DefaultExecutor (executors/default.ts) mediante el mecanismo de reserva de la factoría getExecutor(). El ejecutor:

  • Construye la URL ascendente (buildUrl())
  • Añade encabezados específicos del proveedor (buildHeaders())
  • Transforma el cuerpo de la solicitud (transformRequest())
  • Envía la solicitud HTTP con reintentos y retroceso exponencial
  • Gestiona la renovación de la autenticación cuando es necesario (proveedores OAuth)

Todos los ejecutores extienden BaseExecutor (executors/base.ts, 1170 líneas de código), que proporciona:

  • Lógica común de reintentos
  • Integración con proxy
  • Integración con disyuntor
  • Hooks de registro de uso

Para las respuestas en streaming, el ejecutor devuelve un ReadableStream. El controlador:

  • Pasa el flujo por una transformación SSE (createSSETransformStreamWithLogger)
  • Aplica señales periódicas de actividad para detectar conexiones inactivas
  • Gestiona correctamente la desconexión del cliente (pipeWithDisconnect)
  • Transforma SSE → JSON para clientes sin streaming

Para las respuestas sin streaming, el ejecutor devuelve un objeto JSON analizado que se transmite sin cambios.

Después de la respuesta (correcta o fallida), se registra el uso:

  • prompt_tokens, completion_tokens, cached_tokens de la respuesta
  • cost_usd calculado a partir de los datos de precios
  • latency_ms, status, error_class si se produce un error
  • Se conserva en la tabla usage_history

Los artefactos del registro de llamadas (si están habilitados) se escriben en ${DATA_DIR}/call_logs/.


El controlador principal de solicitudes. A pesar de su tamaño, tiene una estructura clara:

// Pseudoestructura de chatCore.ts
export async function handleChat(request: NextRequest) {
// 1. Autenticación + CORS
await authenticateRequest(request);
applyCorsHeaders(response);
// 2. Validación del cuerpo
const body = await parseRequestBody(request);
// 3. Detección de formato + traducción
const sourceFormat = detectFormat(request);
const targetFormat = getTargetFormat(providerId);
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}
// 4. Enrutamiento de combos
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
const result = await executeOnTarget(target, body);
await recordUsage(result);
return result;
} catch (err) {
// Continuar con el siguiente destino
}
}
// 5. Alternativa de emergencia
return await emergencyFallback(body);
}

A pesar de ser una única función gigantesca, está organizada en secciones comentadas que corresponden al proceso de 5 etapas.

El motor de enrutamiento que resuelve un combo en destinos ordenados.

services/combo.ts
export async function handleComboChat(body, comboId): Promise<ChatResult> {
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
return await handleSingleModel(target, body);
} catch (err) {
log.warn("target failed, trying next", { target, err });
}
}
throw new ComboExhaustedError("All targets failed");
}

Admite 19 estrategias de enrutamiento (consulta src/shared/constants/routingStrategies.ts):

Estrategia Comportamiento
priority Lista ordenada con prioridad para el primer destino
weighted Selección probabilística según el peso de cada destino
round-robin Recorre los destinos en orden de forma cíclica
context-relay Transfiere el contexto entre destinos
fill-first Agota la cuota antes de pasar al siguiente
p2c Potencia de dos opciones
random Selección aleatoria uniforme
least-used Elige el destino con menos usos recientes
cost-optimized Primero, el destino operativo más barato
reset-aware Tiene en cuenta los períodos de restablecimiento del proveedor
reset-window Enrutamiento basado en el período de restablecimiento
headroom Primero, el destino con mayor margen de cuota restante
strict-random Selección verdaderamente uniforme (sin ponderación por calidad)
auto Usa una puntuación de 16 factores (autoCombo/)
lkgp Primero, el último proveedor conocido que funcionaba
context-optimized El más adecuado para solicitudes con contexto largo
fusion Distribuye en paralelo a un panel y luego sintetiza mediante un juez (fusion.ts)

El ejecutor abstracto que extienden los 107 ejecutores. Contiene:

  • buildUrl() — construcción predeterminada de la URL (las subclases la sobrescriben para personalizarla)
  • buildHeaders() — encabezados predeterminados (autenticación, tipo de contenido)
  • transformRequest() — transferencia directa de forma predeterminada
  • execute() — el bucle HTTP principal con reintentos, espera exponencial y disyuntor
open-sse/executors/default.ts
export class DefaultExecutor extends BaseExecutor {
// Gestiona todos los proveedores compatibles con OpenAI/Anthropic
// Los proveedores registran configuraciones (URL, autenticación, encabezados), pero comparten la lógica del ejecutor
}

El comportamiento específico de cada proveedor (encabezados de autenticación, URL base y encabezados de versión) se configura mediante el registro de proveedores, no mediante clases de ejecutor separadas.

---
## Servicios (117 módulos)
Los servicios son **módulos específicos y de propósito único** que los manejadores combinan. Las principales categorías son:
### Enrutamiento y combinación
- `combo.ts` — punto de entrada para solicitudes con enrutamiento combinado
- `services/autoCombo/` — puntuación de 16 factores, 8 estrategias de enrutamiento automático
- `wildcardRouter.ts` — busca coincidencias con rutas comodín (`gpt-*`)
- `modelFamilyFallback.ts` — respaldo intrafamilia T5
### Limitación de tasa y cuota
- `rateLimitManager.ts` — depósito de tokens por clave+proveedor
- `usage.ts` — registro de uso
- `quotaCache.ts` — instantáneas de cuota en memoria
### Cuenta y token
- `tokenRefresh.ts` — renovación de OAuth al recibir 401
- `accountFallback.ts` — cambio a una cuenta alternativa
- `sessionManager.ts` — estado de sesión multiconversación
### Inteligencia
- `intentClassifier.ts` — clasifica la intención de la solicitud
- `taskAwareRouter.ts` — enruta según el tipo de tarea
- `thinkingBudget.ts` — asigna tokens de razonamiento
- `contextManager.ts` — inyecta contexto de enrutamiento
### Resiliencia
- `resilience.ts` — orquestación de reintentos, espera incremental y disyuntores
- `emergencyFallback.ts` — respaldo de último recurso
- `modelDeprecation.ts` — enrutamiento automático a modelos sucesores
### Estado
- `signatureCache.ts` — deduplicación por firma de solicitud
- `volumeDetector.ts` — reducción de carga
- `contextHandoff.ts` — serialización de sesiones
### Compresión
- `compression/` (subdirectorio) — canalización completa de compresión
- 39 archivos que abarcan motores, paquetes de reglas y adaptadores
### Habilidades
- (descrito en [SKILLS.md](./SKILLS.md))
### Memoria
- (descrito en [MEMORY.md](./MEMORY.md))
---
## Ejecutores (más de 75 archivos)
Un archivo por proveedor. Todos extienden `BaseExecutor` y sobrescriben lo que difiere.
### Patrones comunes
Los proveedores se resuelven mediante `getExecutor(providerId)`, que devuelve el ejecutor configurado. Los proveedores compatibles con OpenAI/Anthropic utilizan `DefaultExecutor` (`executors/default.ts`). El comportamiento específico de cada proveedor (URL base, cabeceras de autenticación, versión de la API) se configura en `open-sse/config/providers/`, mientras que las transformaciones del cuerpo de la solicitud se gestionan en `open-sse/translator/`.
La **URL personalizada** se establece mediante la configuración del proveedor:
```ts
// Configuración del proveedor en open-sse/config/providers/
export default {
id: "together",
baseURL: "https://api.together.xyz/v1/chat/completions",
}

La autenticación personalizada se gestiona mediante la configuración de autenticación del registro de proveedores (clave de API, OAuth, perfiles de cabeceras).

Las transformaciones personalizadas del cuerpo de la solicitud (por ejemplo, cuando Anthropic separa system de messages) se registran por proveedor en open-sse/translator/.

### La fábrica de ejecutores
`executors/index.ts` exporta `getExecutor(providerId)`:
```ts
import { getExecutor } from "@omniroute/open-sse/executors";
const executor = getExecutor("anthropic");
const result = await executor.execute({
model: "claude-sonnet-4-5",
messages: [...],
});

La resolución se realiza mediante ExecutorRegistry (executors/registry.ts): cada ejecutor especializado se declara en la tabla integrada de executors/index.ts y se registra mediante registerExecutor(alias, instance) al cargar el módulo; getExecutor() consulta el registro y recurre a un DefaultExecutor memoizado para cualquier proveedor sin una entrada especializada. La asignación completa de alias → ejecutor se especifica mediante la prueba de referencia tests/unit/executor-map-golden.test.ts.


Traduce entre 3 formatos: OpenAI, Anthropic y Gemini, además de la nueva Responses API.

import { needsTranslation, translateRequest } from "@omniroute/open-sse/translator";
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}

Traducciones habituales:

  • OpenAI → Anthropic: campo system independiente, encabezado x-api-key
  • OpenAI → Gemini: contents en lugar de messages, systemInstruction
  • OpenAI → Responses API: matriz input, estado previous_response_id
  • Rol developer → system para proveedores distintos de OpenAI
  • Rol system → combinado con el primer mensaje del usuario para GLM/ERNIE
  • json_schema → responseMimeType + responseSchema de Gemini
  • tools → formato de herramientas específico del proveedor
  • Parámetros de razonamiento (o1, Claude) → equivalentes específicos del proveedor

open-sse/mcp-server/ implementa el servidor del Model Context Protocol:

  • 110 herramientas (gestión de proveedores, combinaciones, memoria, caché, compresión, proxy, habilidades, gamificación, complementos, Notion, Obsidian y corpus local)
  • 3 transportes: stdio, SSE y Streamable HTTP
  • 33 ámbitos para una autorización detallada

Las herramientas se registran como archivos independientes en open-sse/mcp-server/tools/; cada uno exporta un nombre, un esquema, un controlador y un ámbito:

open-sse/mcp-server/tools/getHealth.ts
import { z } from "zod";
export default {
name: "omniroute_get_health",
description: "Get system health snapshot",
scope: "read:health",
inputSchema: z.object({}),
handler: async (_args, ctx) => {
return await getSystemHealth();
},
};
// stdio (uso de la CLI)
startMcpStdio(server);
// SSE (transmisión basada en HTTP)
startMcpSse(server, port);
// Streamable HTTP (MCP moderno)
startMcpStreamable(server, port);

Cada llamada a una herramienta pasa por comprobaciones de ámbito (open-sse/mcp-server/auth/):

if (!hasScope(apiKey, "providers:read")) {
throw new Error("Insufficient scope");
}

open-sse/transformer/ convierte entre los formatos Chat Completions y Responses API.

Responses API es el nuevo formato de OpenAI con conversaciones con estado (previous_response_id). Cuando un cliente envía una solicitud de Responses, OmniRoute:

  1. Convierte internamente Responses → Chat Completions
  2. La envía al proveedor (cualquier proveedor compatible con Chat Completions)
  3. Vuelve a convertir la respuesta al formato de Responses
  4. Transmite la respuesta convertida al cliente

El transformador (transformer/responsesTransformer.ts) proporciona:

createResponsesApiTransformStream(): TransformStream

Esto gestiona:

  • Eventos response.output_item.added
  • Eventos response.output_text.delta
  • Evento response.completed
  • Asignación de llamadas a herramientas (function_call ↔ tool_calls)

open-sse/config/ contiene la capa de configuración:

Archivo Propósito
providerRegistry.ts Registro de modelos de chat basado en el catálogo de 352 proveedores
providerModels.ts Alias de modelos, asignación de formatos
constants.ts Tiempos de espera, límites y códigos de estado
defaultThinkingSignature.ts Firma de razonamiento predeterminada de Claude
modelStrip.ts (en services) Eliminación de campos específica para cada proveedor
interface ProviderConfig {
id: string;
name: string;
baseUrl: string;
authType: "bearer" | "api-key" | "oauth" | "cookie";
executorClass: string;
defaultModel: string;
capabilities: ProviderCapabilities;
models: ModelDefinition[];
}

La validación de Zod durante la carga del módulo garantiza que todas las configuraciones de proveedores sean válidas.


El motor de enrutamiento tiene presupuestos de rendimiento estrictos:

Operación Objetivo Medición
Resolución de combinaciones <10ms Para 50 destinos
Comprobación del límite de solicitudes <1ms Token bucket en memoria
Fallback de familia de modelos <5ms Definiciones de familia en caché
Despacho de enrutamiento de solicitudes <2ms Ruta crítica
Sin E/S bloqueante en la ruta crítica de enrutamiento — Todo asíncrono

❌ Llamadas síncronas a la BD en combo.ts — precalcular y almacenar en caché ❌ Lógica de reintentos en los manejadores — usar retry() del servicio de resiliencia ❌ Acceso directo a la configuración del proveedor — usar los getters de providerRegistry ❌ Cadenas de fallback codificadas de forma rígida — definirlas en modelFamilyFallback.ts ❌ Mutaciones de estado entre solicitudes simultáneas — usar únicamente contexto con ámbito de solicitud


  1. Crear open-sse/services/[serviceName].ts con una responsabilidad específica
  2. Exportar la función principal del manejador y cualquier constante
  3. Añadir pruebas unitarias en tests/unit/services/[serviceName].test.mjs
  4. Integrarlo en el flujo de procesamiento de solicitudes en handlers/chatCore.ts (si está relacionado con el enrutamiento)
  5. Actualizar la lógica de enrutamiento en combo.ts si el servicio afecta a la selección de destinos
  6. Documentarlo en este archivo
  1. Crear open-sse/executors/[provider].ts que extienda BaseExecutor
  2. Registrarlo en config/providerRegistry.ts
  3. Añadirlo a la factoría de executors/index.ts
  4. Añadir pruebas unitarias para el ejecutor
  5. Documentarlo en docs/architecture/ARCHITECTURE.md
  1. Crear o actualizar open-sse/mcp-server/tools/[category]Tools.ts
  2. Definir el esquema Zod para las entradas
  3. Registrar la herramienta en mcp-server/index.ts
  4. Añadirla a la matriz de ámbitos en mcp-server/auth/
  5. Añadir pruebas unitarias


Código fuente de OmniRoute (a58000c7685f)

HagiCode

HagiCode es un espacio de trabajo de programación con agentes, flujos estructurados, ejecución multiagente y vistas de Hero Dungeon.

Convierte ideas en software útil con un flujo de trabajo con agentes más inteligente, rápido y ameno.

Interfaz principal de HagiCode con tema claro
  • SmartLos flujos estructurados convierten la intención en un itinerario ejecutable desde la idea hasta la entrega.
  • EfficientLos flujos multiagente permiten avanzar en paralelo con la investigación, implementación y revisión.
  • FunHero Dungeon hace que las largas sesiones de programación sean visuales y colaborativas.
Visitar HagiCode