Aller au contenu
OmniRoute source

open-sse Architecture (Français)

Pourquoi un package d’espace de travail distinct ?

Section intitulée « Pourquoi un package d’espace de travail distinct ? »

open-sse/ est un espace de travail autonome dans le monorepo OmniRoute pour plusieurs raisons :

  1. Réutilisabilité — open-sse est publié sous le nom @omniroute/open-sse sur npm, afin que d’autres projets puissent l’utiliser indépendamment
  2. Séparation claire — le moteur de streaming est découplé de la couche UI/BDD propre à OmniRoute
  3. Performances — le moteur ne dépend pas de Next.js, ce qui permet des démarrages à froid plus rapides dans les contextes CLI/serverless
  4. Gestion des versions — open-sse peut publier ses versions selon son propre calendrier
package.json
"workspaces": ["open-sse"]

open-sse/
├── index.ts # Point d’entrée public
├── types.d.ts # Exports de types publics
├── package.json # @omniroute/open-sse
├── config/ # Configurations des fournisseurs, constantes, registres
├── executors/ # Exécuteurs HTTP par fournisseur (67 + base.ts/index.ts)
├── handlers/ # Gestionnaires de requêtes (chatCore, responses, etc.)
├── lib/ # Utilitaires internes
├── mcp-server/ # Serveur Model Context Protocol
├── services/ # Environ 298 modules de service
├── transformer/ # Transformateur de format de l’API Responses
├── translator/ # Traduction de formats (OpenAI ↔ Claude ↔ Gemini)
└── utils/ # Utilitaires partagés (journalisation, erreurs, flux, etc.)

| Répertoire | Fichiers | Rôle | | executors/ | 167 | Exécuteurs HTTP par fournisseur (unifiés via la fabrique DefaultExecutor) | | handlers/ | 157 | Points d’entrée des requêtes (chatCore, responses, embeddings) | | services/ | ~536 | Routage, mise en cache, limitation de débit, actualisation, etc. | | translator/ | 56 | Conversion de formats (OpenAI ↔ Claude ↔ Gemini) | | mcp-server/ | 44 | Outils et transports MCP | | utils/ | ~108 | Utilitaires transversaux (journalisation, erreurs, flux) | | config/ | ~339 | Configurations des fournisseurs, constantes, registres |


Chaque requête LLM traverse un pipeline en 5 étapes :

┌──────────────┐
Requête HTTP │ 1. ROUTAGE │ résolution du combo, sélection du modèle
(route Next.js) └──────┬───────┘
│
▼
┌──────────────┐
│ 2. TRADUIRE │ conversion de format (OpenAI ↔ Claude ↔ Gemini)
└──────┬───────┘
│
▼
┌──────────────┐
│ 3. EXÉCUTER │ exécuteur du fournisseur, HTTP, nouvelle tentative, disjoncteur
└──────┬───────┘
│
▼
┌──────────────┐
│ 4. FLUX │ transformation SSE, contre-pression
└──────┬───────┘
│
▼
┌──────────────┐
│ 5. CONSIGNER│ suivi de l’utilisation, journal des appels, classification des erreurs
└──────┬───────┘
│
▼
Réponse HTTP (SSE ou JSON)

Point d’entrée : handleComboChat() dans services/combo.ts

Résout la requête en un tuple concret (provider, model, account, credentials) :

  • Rechercher le combo par ID (ou créer un combo virtuel pour les modèles auto/*)
  • Appliquer la stratégie de routage (priorité, pondération, round-robin, etc.)
  • Exclure les fournisseurs défaillants (disjoncteur)
  • Choisir la prochaine cible viable

Pour les modèles auto/*, cette étape :

  • Exécute également l’algorithme de notation à 16 facteurs (services/autoCombo/)
  • Sélectionne une paire provider+model en fonction de l’état de santé, du coût, de la latence, etc.

Si le format source (par exemple, OpenAI) diffère du format cible (par exemple, Claude), la requête est traduite :

  • Prompt système → message système
  • Définitions des outils → format d’outil propre au fournisseur
  • Paramètres de raisonnement/réflexion → équivalents propres au fournisseur
  • Normalisation des rôles de message (developer → system pour les fournisseurs autres qu’OpenAI)

Le fichier translator/index.ts expose :

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

Point d’entrée : getExecutor(providerId).execute(request, options)

Tous les fournisseurs utilisent DefaultExecutor (executors/default.ts) comme solution de repli via la fabrique getExecutor(). L’exécuteur :

  • Construit l’URL en amont (buildUrl())
  • Ajoute les en-têtes propres au fournisseur (buildHeaders())
  • Transforme le corps de la requête (transformRequest())
  • Envoie la requête HTTP avec de nouvelles tentatives et un délai exponentiel
  • Gère l’actualisation de l’authentification si nécessaire (fournisseurs OAuth)

Tous les exécuteurs étendent BaseExecutor (executors/base.ts, 1170 lignes de code), qui fournit :

  • Une logique commune de nouvelle tentative
  • L’intégration du proxy
  • L’intégration du disjoncteur
  • Des hooks de consignation de l’utilisation

Pour les réponses en streaming, l’exécuteur renvoie un ReadableStream. Le gestionnaire :

  • Achemine le flux à travers une transformation SSE (createSSETransformStreamWithLogger)
  • Applique des signaux périodiques pour détecter les connexions interrompues
  • Gère proprement la déconnexion du client (pipeWithDisconnect)
  • Transforme SSE → JSON pour les clients sans streaming

Pour les réponses sans streaming, l’exécuteur renvoie un objet JSON analysé qui est transmis sans modification.

Après la réponse (réussie ou échouée), les données d’utilisation sont enregistrées :

  • prompt_tokens, completion_tokens, cached_tokens provenant de la réponse
  • cost_usd calculé à partir des données tarifaires
  • latency_ms, status, error_class en cas d’échec
  • Conservées dans la table usage_history

Les artefacts des journaux d’appels (s’ils sont activés) sont écrits dans ${DATA_DIR}/call_logs/.


Le gestionnaire principal des requêtes. Malgré sa taille, sa structure est claire :

// Pseudo-structure de chatCore.ts
export async function handleChat(request: NextRequest) {
// 1. Authentification + CORS
await authenticateRequest(request);
applyCorsHeaders(response);
// 2. Validation du corps
const body = await parseRequestBody(request);
// 3. Détection du format + traduction
const sourceFormat = detectFormat(request);
const targetFormat = getTargetFormat(providerId);
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}
// 4. Routage du combo
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) {
// Continuer avec la cible suivante
}
}
// 5. Solution de repli d'urgence
return await emergencyFallback(body);
}

Bien qu’il s’agisse d’une seule fonction gigantesque, elle est organisée en sections commentées qui correspondent au pipeline en 5 étapes.

Le moteur de routage qui convertit un combo en une liste ordonnée de cibles.

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("échec de la cible, tentative avec la suivante", {
target,
err,
});
}
}
throw new ComboExhaustedError("Toutes les cibles ont échoué");
}

Prend en charge 19 stratégies de routage (voir src/shared/constants/routingStrategies.ts) :

Stratégie Comportement
priority Liste ordonnée donnant la priorité à la première cible
weighted Sélection probabiliste selon le poids de chaque cible
round-robin Parcours cyclique des cibles dans l’ordre
context-relay Transfert du contexte entre les cibles
fill-first Épuise le quota avant de passer à la cible suivante
p2c Puissance de deux choix
random Sélection aléatoire uniforme
least-used Sélectionne celle ayant été la moins utilisée récemment
cost-optimized Cible opérationnelle la moins chère en premier
reset-aware Tient compte des fenêtres de réinitialisation du fournisseur
reset-window Routage fondé sur la fenêtre de réinitialisation
headroom Plus grande marge de quota restante en premier
strict-random Répartition véritablement uniforme (sans pondération par la qualité)
auto Utilise une notation à 16 facteurs (autoCombo/)
lkgp Dernier fournisseur connu comme opérationnel en premier
context-optimized Idéal pour les requêtes à contexte long
fusion Diffuse en parallèle vers un panel, puis effectue une synthèse via un juge (fusion.ts)

L’exécuteur abstrait dont héritent les 107 exécuteurs. Il contient :

  • buildUrl() — construction de l’URL par défaut (les sous-classes la redéfinissent pour les cas personnalisés)
  • buildHeaders() — en-têtes par défaut (authentification, type de contenu)
  • transformRequest() — transmission directe par défaut
  • execute() — la boucle HTTP principale avec nouvelles tentatives, temporisation exponentielle et disjoncteur
open-sse/executors/default.ts
export class DefaultExecutor extends BaseExecutor {
// Gère tous les fournisseurs compatibles avec OpenAI/Anthropic
// Les fournisseurs enregistrent leurs configurations (URL, authentification, en-têtes), mais partagent la logique de l'exécuteur
}

Le comportement propre à chaque fournisseur (en-têtes d’authentification, URL de base, en-têtes de version) est configuré au moyen du registre des fournisseurs, et non de classes d’exécuteur distinctes.

---
## Services (117 modules)
Les services sont des **modules ciblés et dédiés à une seule tâche** que les gestionnaires composent. Les grandes catégories sont les suivantes :
### Routage et combinaison
- `combo.ts` — point d’entrée des requêtes utilisant le routage combiné
- `services/autoCombo/` — notation à 16 facteurs, 8 stratégies de routage automatique
- `wildcardRouter.ts` — fait correspondre les routes génériques (`gpt-*`)
- `modelFamilyFallback.ts` — repli T5 au sein d’une même famille
### Limitation du débit et quota
- `rateLimitManager.ts` — compartiment à jetons par clé+fournisseur
- `usage.ts` — enregistrement de l’utilisation
- `quotaCache.ts` — instantanés des quotas en mémoire
### Compte et jeton
- `tokenRefresh.ts` — actualisation OAuth en cas de réponse 401
- `accountFallback.ts` — basculement vers un autre compte
- `sessionManager.ts` — état des sessions multitours
### Intelligence
- `intentClassifier.ts` — classe l’intention de la requête
- `taskAwareRouter.ts` — effectue le routage selon le type de tâche
- `thinkingBudget.ts` — alloue les jetons de raisonnement
- `contextManager.ts` — injecte le contexte de routage
### Résilience
- `resilience.ts` — orchestration des nouvelles tentatives, du délai exponentiel et du disjoncteur
- `emergencyFallback.ts` — repli de dernier recours
- `modelDeprecation.ts` — routage automatique vers les modèles successeurs
### État
- `signatureCache.ts` — déduplication selon la signature de la requête
- `volumeDetector.ts` — délestage de charge
- `contextHandoff.ts` — sérialisation des sessions
### Compression
- `compression/` (sous-répertoire) — pipeline de compression complet
- 39 fichiers couvrant les moteurs, les ensembles de règles et les adaptateurs
### Compétences
- (décrites dans [SKILLS.md](./SKILLS.md))
### Mémoire
- (décrite dans [MEMORY.md](./MEMORY.md))
---
## Exécuteurs (plus de 75 fichiers)
Un fichier par fournisseur. Ils étendent tous `BaseExecutor` et redéfinissent les éléments qui diffèrent.
### Modèles courants
Les fournisseurs sont résolus via `getExecutor(providerId)`, qui renvoie l’exécuteur configuré. Les fournisseurs compatibles avec OpenAI/Anthropic utilisent `DefaultExecutor` (`executors/default.ts`). Le comportement propre à chaque fournisseur (URL de base, en-têtes d’authentification, version de l’API) est configuré dans `open-sse/config/providers/`, tandis que les transformations du corps des requêtes sont gérées dans `open-sse/translator/`.
L’**URL personnalisée** est définie via la configuration du fournisseur :
```ts
// Configuration du fournisseur dans open-sse/config/providers/
export default {
id: "together",
baseURL: "https://api.together.xyz/v1/chat/completions",
}

L’authentification personnalisée est gérée par l’intermédiaire de la configuration d’authentification du registre des fournisseurs (clé d’API, OAuth, profils d’en-têtes).

Les transformations personnalisées du corps de la requête (par exemple, Anthropic séparant system de messages) sont enregistrées par fournisseur dans open-sse/translator/.

### La fabrique d’exécuteurs
`executors/index.ts` exporte `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 résolution passe par ExecutorRegistry (executors/registry.ts) : chaque exécuteur spécialisé est déclaré dans la table intégrée de executors/index.ts et enregistré via registerExecutor(alias, instance) lors du chargement du module ; getExecutor() consulte le registre et se rabat sur un DefaultExecutor mémoïsé pour tout fournisseur ne disposant pas d’une entrée spécialisée. La correspondance complète alias → exécuteur est définie par le test de référence tests/unit/executor-map-golden.test.ts.


Effectuez des traductions entre 3 formats : OpenAI, Anthropic, Gemini, ainsi que la nouvelle Responses API.

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

Traductions courantes :

  • OpenAI → Anthropic : champ system distinct, en-tête x-api-key
  • OpenAI → Gemini : contents au lieu de messages, systemInstruction
  • OpenAI → Responses API : tableau input, état previous_response_id
  • Rôle developer → system pour les fournisseurs autres qu’OpenAI
  • Rôle system → fusionné dans le premier message utilisateur pour GLM/ERNIE
  • json_schema → responseMimeType + responseSchema de Gemini
  • tools → format d’outil propre au fournisseur
  • Paramètres de raisonnement (o1, Claude) → équivalents propres au fournisseur

open-sse/mcp-server/ implémente le serveur Model Context Protocol :

  • 110 outils (gestion des fournisseurs, combinaisons, mémoire, cache, compression, proxy, compétences, ludification, extensions, Notion, Obsidian, corpus local)
  • 3 transports : stdio, SSE, HTTP diffusé en continu
  • 33 portées pour une autorisation précise

Les outils sont enregistrés sous forme de fichiers autonomes dans open-sse/mcp-server/tools/, chacun exportant un nom, un schéma, un gestionnaire et une portée :

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 (utilisation en CLI)
startMcpStdio(server);
// SSE (diffusion en continu basée sur HTTP)
startMcpSse(server, port);
// HTTP diffusable en continu (MCP moderne)
startMcpStreamable(server, port);

Chaque appel d’outil passe par des vérifications de portée (open-sse/mcp-server/auth/) :

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

open-sse/transformer/ effectue la conversion entre les formats Chat Completions et Responses API.

Responses API est le nouveau format d’OpenAI avec des conversations avec état (previous_response_id). Lorsqu’un client envoie une requête Responses, OmniRoute :

  1. Convertit en interne Responses → Chat Completions
  2. Envoie la requête au fournisseur (tout fournisseur prenant en charge Chat Completions)
  3. Reconvertit la réponse au format Responses
  4. Diffuse en continu la réponse convertie au client

Le transformateur (transformer/responsesTransformer.ts) fournit :

createResponsesApiTransformStream(): TransformStream

Celui-ci gère :

  • Les événements response.output_item.added
  • Les événements response.output_text.delta
  • L’événement response.completed
  • La mise en correspondance des appels d’outils (function_call ↔ tool_calls)

open-sse/config/ contient la couche de configuration :

Fichier Objectif
providerRegistry.ts Registre des modèles de chat du catalogue de 352 fournisseurs
providerModels.ts Alias de modèles, mise en correspondance des formats
constants.ts Délais d’expiration, limites, codes d’état
defaultThinkingSignature.ts Signature de raisonnement Claude par défaut
modelStrip.ts (dans services) Suppression des champs propre à chaque fournisseur
interface ProviderConfig {
id: string;
name: string;
baseUrl: string;
authType: "bearer" | "api-key" | "oauth" | "cookie";
executorClass: string;
defaultModel: string;
capabilities: ProviderCapabilities;
models: ModelDefinition[];
}

La validation Zod au chargement du module garantit que toutes les configurations des fournisseurs sont valides.


Le moteur de routage est soumis à des budgets de performance stricts :

Opération Objectif Mesure
Résolution des combinaisons <10ms Pour 50 cibles
Vérification de la limitation de débit <1ms Seau à jetons en mémoire
Repli par famille de modèles <5ms Définitions de familles en cache
Distribution des requêtes de routage <2ms Chemin critique
Aucune E/S bloquante dans le chemin critique du routage — Entièrement asynchrone

❌ Appels synchrones à la base de données dans combo.ts — précalculer et mettre en cache ❌ Logique de nouvelle tentative dans les gestionnaires — utiliser retry() du service de résilience ❌ Accès direct à la configuration des fournisseurs — utiliser les accesseurs de providerRegistry ❌ Chaînes de repli codées en dur — les définir dans modelFamilyFallback.ts ❌ Mutations d’état entre des requêtes concurrentes — utiliser uniquement un contexte propre à chaque requête


  1. Créer open-sse/services/[serviceName].ts avec une responsabilité bien définie
  2. Exporter la fonction principale du gestionnaire ainsi que les éventuelles constantes
  3. Ajouter des tests unitaires dans tests/unit/services/[serviceName].test.mjs
  4. L’intégrer au pipeline de traitement des requêtes dans handlers/chatCore.ts (s’il concerne le routage)
  5. Mettre à jour la logique de routage dans combo.ts si le service affecte la sélection des cibles
  6. Le documenter dans ce fichier
  1. Créer open-sse/executors/[provider].ts étendant BaseExecutor
  2. L’enregistrer dans config/providerRegistry.ts
  3. L’ajouter à la fabrique dans executors/index.ts
  4. Ajouter des tests unitaires pour l’exécuteur
  5. Le documenter dans docs/architecture/ARCHITECTURE.md
  1. Créer ou mettre à jour open-sse/mcp-server/tools/[category]Tools.ts
  2. Définir le schéma Zod des entrées
  3. Enregistrer l’outil dans mcp-server/index.ts
  4. L’ajouter à la matrice des portées dans mcp-server/auth/
  5. Ajouter des tests unitaires


Code source d’OmniRoute (a58000c7685f)

HagiCode

HagiCode est un espace de développement agentique qui associe workflows structurés, exécution multi-agent et vues Hero Dungeon.

Transformez vos idées en logiciels utiles grâce à un workflow agentique plus intelligent, rapide et agréable.

Interface principale de HagiCode en thème clair
  • SmartDes workflows structurés transforment une intention en parcours exécutable, de l’idée à la livraison.
  • EfficientLes workflows multi-agents font avancer recherche, réalisation et revue en parallèle.
  • FunHero Dungeon rend les longues sessions de code plus visuelles et collaboratives.
Visiter HagiCode