Engineering

GPU local, OpenRouter, ou les deux : router chaque rôle d'inférence indépendamment

Cinq rôles, quatre politiques, un budget partagé — et un contrôle de compatibilité qui refuse de laisser un encodeur cloud empoisonner discrètement un index construit par un GPU local.

Alper Üzmezler· Sep 18, 2026 · 13 min de lecture

« Ajouter un repli cloud » ressemble à un simple drapeau de configuration. Pour un système de recherche documentaire, cela tient davantage d'un problème d'intégrité des données déguisé en drapeau de configuration.

La génération de texte est sans état — si un fournisseur est lent, on en utilise un autre, et personne ne s'en aperçoit après coup. L'embedding, non. L'embedding écrit dans une table que vous allez interroger pendant des mois, et une requête n'a de sens que si l'encodeur qui a vectorisé la requête est en accord avec celui qui a vectorisé les lignes.

L'intégration cloud de Fantom MCP Server, c'est essentiellement la mécanique nécessaire pour rendre cela sûr.

Cinq rôles, routés séparément

Il n'existe pas d'interrupteur unique « utiliser le cloud », car les cinq rôles d'inférence ont des exigences réellement différentes. Chacun est routé selon sa propre politique :

GPU locaux hôtes sidecar VRAM réelle OpenRouter conteneurs virtuels aucun GPU requis code-embedding aggregate embedding backup reranker cloud code-assistant cloud rlm local Un exemple illustratif — chaque rôle se configure indépendamment et se modifie à l'exécution

Les quatre politiques :

Politique Comportement
local GPU locaux et hôtes sidecar uniquement. Rien ne quitte le réseau.
cloud OpenRouter uniquement. Aucun GPU nécessaire.
aggregate Les deux, dans un seul pool réparti, puisant dans la même file. (par défaut)
backup Le local d'abord, le cloud gardé en réserve.

aggregate est la valeur par défaut, car c'est celle qui exploite le matériel que vous avez déjà payé tout en absorbant les pics.

Le contrôle qui rend tout cela sûr

En mode aggregate, les vecteurs locaux et cloud atterrissent dans la même table, entrelacés, au cours de la même exécution. Si les deux piles de service ne produisent pas des vecteurs interchangeables, la distance cosinus cesse d'être comparable et la recherche se dégrade silencieusement — sans aucun moyen de savoir quelles lignes viennent d'où.

La compatibilité n'est donc pas un avertissement. C'est une condition préalable stricte : un fournisseur qui n'a pas passé le test ne doit jamais traiter le moindre texte.

Textes de test → encodés deux fois → cosinus deux à deux fournisseur de référence 2560 dimensions fournisseur candidat doit correspondre exactement seuil 0,99 0,95 1,00 0,976 local vs cloud sous le seuil — rejeté ≥ 0,99 chaque paire de test dimensions exactes — écriture permise L'encodeur de requête provient toujours du même pool que celui qui a bâti les lignes.

Le contrôle est volontairement dépourvu de subtilité : encoder les mêmes textes de test par les deux routes, calculer la similarité cosinus deux à deux sur chaque paire correspondante, exiger que le minimum dépasse 0,99, et exiger que la dimension renvoyée corresponde exactement à la largeur configurée.

Le chiffre intéressant est celui qui a échoué. Le cosinus mesuré entre le modèle local et le modèle cloud est de 0,976 — assez proche pour paraître correct lors d'une vérification ponctuelle, confortablement sous le seuil, et précisément la raison d'être de la politique cloud. Ces deux encodeurs ne sont pas interchangeables : si vous voulez utiliser celui du cloud, l'index entier doit être construit à partir de lui. Ce n'est pas un contournement ; c'est la politique qui fait son travail.

Ce qui signifie que la référence n'est pas toujours locale. Sous les trois politiques où les GPU interviennent, la référence doit être locale, car l'accord avec les lignes déjà présentes dans la table est exactement la propriété testée. Sous cloud, comparer à un modèle local n'aurait aucun sens — aucun vecteur local n'est jamais écrit — la référence devient donc un autre fournisseur cloud, et le contrôle reste par ailleurs inchangé. Il n'est jamais désactivé.

La limite, énoncée plutôt qu'enfouie

Quand il n'existe qu'un seul fournisseur cloud, il n'y a pas de pair, et la cohérence inter-fournisseurs devient vide de sens : un fournisseur ne peut pas être en désaccord avec lui-même, et c'est lui seul qui écrit chaque ligne. Le contrôle vérifie alors la dimension et l'épinglage en amont, et rien d'autre.

C'est une réduction réelle de la couverture. Un unique amont épinglé peut tout de même modifier sa pile de service derrière le même épinglage au fil du temps — un hôte redéployant une quantification différente du même identifiant de modèle — et sous un régime cloud à fournisseur unique, rien ne le détecte. Bon à savoir si vous utilisez cette configuration.

Pourquoi l'embedding est épinglé et le reranking ne l'est pas

Deux amonts OpenRouter servant le même identifiant de modèle ne garantissent pas des vecteurs identiques. Une route d'embedding non épinglée peut donc scinder un espace vectoriel entre deux requêtes, ce qui revient à la même corruption par un chemin plus discret. Chaque route d'embedding doit donc nommer son fournisseur amont, et le système échoue en mode fermé si ce n'est pas le cas.

Le reranking est sans état — il attribue des scores à une liste de candidats et n'écrit rien — il n'a donc besoin d'aucun épinglage.

Un budget unique, placé au-dessus de tous les fournisseurs

Voici le piège qui façonne tout le chemin cloud : OpenRouter limite le débit par clé, pas par appelant.

Chaque sidecar reçoit la même clé. Trois fournisseurs plafonnés chacun à huit requêtes en vol, cela fait vingt-quatre requêtes sur un unique budget partagé. Un plafond par fournisseur ne peut pas protéger la limite — le plafond doit se situer au-dessus de tous.

Pool global de jetons — un jeton = une requête cloud simultanée plafond 429 → moitié + jitter concurrence ≈ (requêtes par seconde) × (secondes moyennes par requête) — loi de Little, redérivée quand la latence dérive défaut si rien n'a pu être lu : 4 jetons · plafond strict en cas de faute de frappe : 256 Les GPU locaux ne touchent jamais ce pool un backoff cloud ne doit pas immobiliser le matériel continuent de tirer du travail Équitable entre projets files d'attente par projet, servies en tourniquet un gros projet ne peut pas affamer les autres

Quelques détails méritent d'être mis en lumière.

L'unité du jeton est la concurrence, pas les requêtes par minute. Une valeur rate_limit découverte est un chiffre en RPM ; elle est donc convertie à l'aide de la latence aller-retour mesurée par le pool lui-même — la loi de Little — ce qui signifie qu'elle se recalcule à mesure que la latence dérive, au lieu d'être figée sur une valeur datant du démarrage.

Un RPM jugé fiable est aussi appliqué directement, sous forme de fenêtre glissante, afin qu'une estimation de latence brièvement trop élevée ne puisse pas déborder le compte. Mais ce second garde-fou n'est armé que pour un RPM auquel on peut se fier. Un compte approvisionné dont la clé annonce une limite de débit dégénérée — ou aucune, ce qui est la forme moderne — est dimensionné à partir de son solde de crédit et ne reçoit aucune fenêtre glissante. L'armer sur un « 1 par minute » vestigial était strictement pire : cela réduisait tout le déploiement cloud à une requête par minute.

Le backoff est adaptatif, jamais une nouvelle tentative immédiate. Un 429 divise par deux le budget effectif et déclenche une période de refroidissement avec jitter ; une fenêtre durablement propre le fait remonter un jeton à la fois vers le plafond.

Les valeurs par défaut sont délibérément timides. Quatre jetons lorsque rien n'a pu être découvert, car supposer une marge que l'on n'a pas pu lire est le meilleur moyen d'obtenir une tempête de 429 et une table à moitié écrite. Et un plafond strict de 256, car une faute de frappe dans un champ de surcharge ne devrait pas saturer le compte.

Les GPU locaux en sont entièrement exemptés. Le travail est tiré par fournisseur : un fournisseur cloud bloqué cesse simplement de tirer, et les locaux continuent. Une limite de débit cloud ralentit le cloud ; elle n'immobilise pas votre matériel.

Gestion des clés

La clé OpenRouter est en écriture seule du point de vue de Fantom MCP Server. Elle arrive via une requête d'administration, repart immédiatement dans la trame WebSocket vers le sidecar qui l'utilisera, et n'est jamais écrite dans la configuration, jamais journalisée, jamais renvoyée par le moindre point de terminaison, et jamais conservée au-delà de l'appel.

Ce qui est persisté est uniquement ce qu'il est sûr de persister : quels rôles ont un modèle associé, quel amont est épinglé, les modes de routage, et le fait qu'un envoi a eu lieu. Réémettre l'envoi sans clé est le cas normal pour changer un mode ou un modèle — le sidecar fusionne un envoi partiel avec l'entrée qu'il détient déjà.

Ce qui tourne où, par défaut

  • Rerankerqwen/qwen3-reranker-8b
  • Assistant de codepoolside/laguna-s-2.1
  • Bac à sable RLMdeepseek/deepseek-v4-flash, avec un mode de repli en local uniquement

Les modèles d'embedding sont choisis par déploiement, car c'est ce choix qui détermine ce qu'est votre index. La table vectorielle du code est un espace unique à 2560 dimensions ; tout ce qui précède existe pour qu'elle le reste.

L'essentiel

Rien de tout cela ne rend le cloud plus rapide. Ce que cela apporte, c'est la capacité d'affirmer quelque chose de précis : que chaque ligne de l'index a été écrite par un encodeur dont l'interchangeabilité avec celui qui encodera votre requête a été prouvée, qu'aucun projet isolé ne peut dévorer le compte, et que lorsque le cloud est bridé, les GPU continuent de travailler.

« Ajouter un repli cloud » est bel et bien un drapeau de configuration. Tout ce billet décrit ce qui doit être vrai en dessous avant que ce drapeau puisse être basculé en toute sécurité.


Fantom MCP Server est disponible en source ouverte sur github.com/Project-SandStar/FantomMcpServer. Billets précédents : la flotte Sidecar et le RLM · l'intelligence de code multilingue.