Aller au contenu

Découvrir la passerelle

Monter les services de la passerelle sur votre poste, y router un appel vers un moteur d'inférence local, et constater qu'il est passé par elle.

Les services de la passerelle sont trois : la passerelle, sa base et la console d’administration. La passerelle authentifie, applique la politique du projet, pilote la session, journalise et révoque ; la base et la console la servent (livre blanc sécurité V3, sections 3.1 et 06). Ce tutoriel les monte sur votre poste pour vous montrer la première de ces fonctions : un appel au modèle authentifié, relayé et attribué. Il appartient au parcours Installer l’assistant de code complet, et sert aussi à qui veut intégrer Lemniscate à sa stack.

Cette page n’est pas une procédure de production, et le dépôt n’en contient pas. Il lève chaque pièce séparément : gateway-db/docker-compose.yml pour la base, un fichier d’image pour la passerelle, un autre pour la console. Il versionne deux manifestes Kubernetes, durcissement/kubernetes/llm-gateway.yaml et durcissement/kubernetes/gateway-admin.yaml, qui fixent les contraintes d’exécution et rien d’autre : l’image, les secrets, les sondes et les ressources appartiennent au client. Aucun fichier ne décrit le trio ensemble : ni composition, ni installeur, ni suite d’étapes vérifiée. Le seul chemin de déploiement complet versionné vise un hébergeur particulier. Cette page monte les trois composants sur votre poste, pour que vous voyiez comment ils s’articulent avant de les exploiter.

Le montage obtenu n’est pas sécurisé : la passerelle écoute en HTTP clair, la console d’administration n’a qu’un seul jeu d’identifiants, partagé par toute personne qui l’ouvre, et les mots de passe de la base viennent de fichiers d’exemple publiés dans le dépôt. Ne le reproduisez pas ailleurs que sur votre machine.

Dans l’architecture de référence, la passerelle se raccorde au moteur d’inférence de votre périmètre par une API compatible OpenAI, le modèle est un modèle à poids ouverts, et aucun flux ne sort (livre blanc sécurité V3, sections 3.3 et 3.4). Ce tutoriel suit ce chemin : Ollama, sur votre poste, tient le rôle du serveur d’inférence, et sert le modèle à poids ouverts qwen2.5-coder:14b par son API compatible OpenAI. Aucun appel ne quitte la machine.

Le montage simplifie l’architecture de référence sur un point : les quatre rôles tiennent sur une seule machine. Voici ce que cela change par rapport à un déploiement.

  • Les trois services de la passerelle s’exécutent sur un hôte dédié, distinct du poste, de l’hôte d’exécution et du serveur d’inférence. Ici, ils partagent votre poste avec le moteur.
  • Les flux entre composants sont chiffrés en TLS, et votre filtrage réseau n’autorise que l’hôte de la passerelle à joindre le moteur (livre blanc sécurité V3, sections 6.2 et 8.2). Ici, la passerelle joint le moteur en HTTP sur l’interface locale de la machine, où Ollama écoute par défaut.
  • Les identités viennent de votre annuaire. Ici, une clé d’administration et un couple d’identifiants en tiennent lieu.
  • Une session agent s’exécute dans un bac à sable, sur l’hôte d’exécution. Ce tutoriel s’arrête à l’appel de modèle : il n’ouvre aucune session.

Comptez une trentaine de minutes. Vous avez besoin de trois terminaux : la suite les appelle A, B et C, et chacun démarre à la racine de votre copie du dépôt. Le terminal A sert aux étapes 1 à 5 puis 13 à 15 ; B et C portent chacun un service qui reste au premier plan.

Terminal A. gateway-db/docker-compose.yml lit un fichier .env qui n’est pas versionné. Seul l’exemple l’est :

Fenêtre de terminal
cd gateway-db
cp .env.example .env

Résultat attendu : un fichier gateway-db/.env portant POSTGRES_USER=admin, POSTGRES_PASSWORD=admin_secret et POSTGRES_DB=proxy, ainsi que les mots de passe de développement des deux rôles de service. Ces mots de passe sont publiés dans le dépôt : ils ne valent que pour un poste de découverte.

Toujours dans le terminal A, dans gateway-db :

Fenêtre de terminal
docker compose up -d --wait

Résultat attendu : la commande rend la main une fois le conteneur gateway-db sain, et docker compose ps le montre en cours d’exécution, port 6000 de votre machine vers 5432 du conteneur. Ce premier démarrage, et lui seul, exécute init.sql, qui pose le schéma complet, puis dev-role-passwords.sql, qui donne un mot de passe de développement aux deux rôles de service lemniscate_gateway et lemniscate_console. La base est vide : aucun moteur déclaré, aucun modèle, aucun point d’appel, aucun compte. Tout ce que la passerelle sert, vous le déclarez vous-même au cours de ce tutoriel ; c’est aussi ce qu’un exploitant voit sur une installation neuve.

init.sql décrit l’état cible du schéma, mais il ne tient pas le registre des migrations. Rejouer la suite migrations/ sur cette base échouerait dès la première, qui recrée des tables existantes. Vous inscrivez donc les migrations comme appliquées, sans les exécuter :

Fenêtre de terminal
export DATABASE_URL=postgresql://admin:admin_secret@localhost:6000/proxy
./migrate.sh --baseline 029

Résultat attendu : une ligne inscrite sans exécution : … par migration, puis jalon posé jusqu'à 029, suivi du nombre d’inscriptions. Le jalonnage est une opération à usage unique, réservée à une base dont le schéma est déjà à jour.

Le numéro du jalon est celui de la dernière migration du dépôt : le jalon doit couvrir tout ce qu’init.sql a déjà posé. Un jalon trop bas laisse des migrations à appliquer sur un schéma qui les contient déjà. La liste qui fait foi est celle qu’imprime ./migrate.sh --status.

Fenêtre de terminal
./migrate.sh

Résultat attendu : base à jour, aucune migration à appliquer. À partir d’ici, cette commande seule applique les migrations écrites après celles-ci.

La passerelle construite pour ce montage refuse de démarrer sans ADMIN_API_KEY : la base ne sème aucun compte, et c’est cette variable qui crée le compte admin au premier démarrage.

Fenêtre de terminal
export ADMIN_API_KEY="$(printf 'sk-lemniscate-%s' "$(openssl rand -hex 24)")"
echo "$ADMIN_API_KEY"

Résultat attendu : une ligne commençant par sk-lemniscate- suivie de 48 caractères hexadécimaux. Copiez-la : le terminal C en a besoin à l’étape 12, et elle sert de clé d’appel à l’étape 14.

Terminal B, à la racine du dépôt :

Fenêtre de terminal
cd gateway-admin
npm install
LEMNISCATE_DEPLOYMENT_PROFILE=serverless npm run build

Résultat attendu : la construction annonce Construction de gateway-admin... (profil : serverless), puis produit dist/client.css, dist/client.js et dist/server.js. Sans cette construction, la console n’a rien à servir.

Toujours dans le terminal B, dans gateway-admin. ADMIN_USER et ADMIN_PASS sont obligatoires : sans elles, la console refuse de démarrer. DATABASE_URL désigne le rôle de service de la console, lemniscate_console : la console refuse de démarrer avec le compte propriétaire de la base.

Fenêtre de terminal
DATABASE_URL=postgresql://lemniscate_console:console_secret@localhost:6000/proxy \
ADMIN_USER=admin ADMIN_PASS=change-me npm run start

Résultat attendu : Admin UI running at http://localhost:6002, suivi de deux lignes qui disent ce qui garde la console et quel référentiel de comptes elle sert. Ouvrez cette adresse ; le navigateur demande un identifiant et un mot de passe, ce sont les deux valeurs ci-dessus. Ce couple est le seul de l’installation, et toute personne qui le détient détient la console entière.

La passerelle relaie les appels vers un moteur d’inférence, que la console nomme Provider. La base n’en contient aucun : la déclaration est votre premier acte d’exploitant.

Terminal A. Vérifiez d’abord que le moteur répond et qu’il sert le modèle :

Fenêtre de terminal
curl -s http://127.0.0.1:11434/v1/models

Résultat attendu : un objet JSON dont la liste data contient une entrée d’identifiant qwen2.5-coder:14b. Sans réponse, démarrez Ollama avant de continuer.

Dans la console, entrée Endpoints de la barre latérale, section Providers, dépliez Add provider et renseignez :

  • Slug : moteur-local
  • Name : Moteur local
  • Base URL : http://127.0.0.1:11434
  • API Key Env : laissez vide
  • API Key Header : laissez vide

L’adresse commence par http:// : une case à cocher apparaît sous le formulaire. Son texte dit que les requêtes et les réponses circulent en clair sur ce trajet, et se termine par I accept this for this provider. Cochez-la, puis validez avec Add. Sans la case, la console refuse la déclaration.

Résultat attendu : une ligne moteur-local apparaît dans le tableau Providers, avec son adresse.

L’acceptation du trajet en clair ne vaut que pour ce moteur, et pour ce montage où le trajet ne quitte pas la machine. Dans un déploiement, le moteur se déclare en https://.

Les deux champs de clé restent vides parce qu’Ollama n’en demande pas. Pour un moteur qui exige une clé, API Key Env porte le nom d’une variable d’environnement de la passerelle, et non la clé elle-même.

Même entrée, section Models, dépliez Add model et renseignez :

  • Slug : qwen2.5-coder:14b
  • Name : Qwen2.5 Coder 14B
  • Provider : Moteur local
  • Input price et Output price : laissez 0

Validez avec Add.

Résultat attendu : une ligne apparaît dans le tableau Models, rattachée au moteur moteur-local. Le slug est l’identifiant que la passerelle envoie au moteur : il doit être celui que l’étape 8 a lu dans la liste du moteur.

Même entrée, section Endpoints, dépliez Add endpoint :

  • Name : code-principal
  • Model : Qwen2.5 Coder 14B

Validez avec Add.

Résultat attendu : une ligne code-principal apparaît dans le tableau Endpoints, active. Le nom de l’endpoint est le premier segment de l’URL que les appelants utilisent ; il détermine à la fois le moteur joint et le modèle employé. Il nomme un usage, et non un modèle : vous pourrez changer le modèle servi sans changer ce nom.

Terminal C, à la racine du dépôt :

Fenêtre de terminal
cd llm-gateway
npm install
LEMNISCATE_DEPLOYMENT_PROFILE=serverless npm run build

Résultat attendu : npm install s’achève sans erreur, puis la construction annonce Construction du llm-gateway... (profil : serverless), liste les artefacts produits dans dist/ et se termine par Construction terminée.

Le profil vaut ici pour la même raison qu’à l’étape 6. L’artefact fermé, celui que npm run build produit sans profil et que le contrôle d’absence de sortie réseau inspecte, n’amorce aucun compte depuis ADMIN_API_KEY et exige une licence de déploiement. Ce n’est pas celui de ce montage.

Toujours dans le terminal C, dans llm-gateway. Les deux variables sont obligatoires. DATABASE_URL désigne le rôle de service de la passerelle, lemniscate_gateway : la passerelle refuse de démarrer avec le compte propriétaire de la base.

Fenêtre de terminal
DATABASE_URL=postgresql://lemniscate_gateway:gateway_secret@localhost:6000/proxy \
ADMIN_API_KEY=<la clé affichée à l'étape 5> \
npm run start

Résultat attendu : le journal de démarrage commence par ces lignes.

[ADMIN] administrator account "admin" created from ADMIN_API_KEY
[ADMIN] administrator account "admin" put on the unlimited "administrator" plan
[ÉCOUTE] PLAINTEXT — http://localhost:6001. No TLS_CERT_FILE/TLS_KEY_FILE was given, …
[MOTEUR] no client certificate configured (…)

La première dit que le compte admin vient d’être créé, avec votre clé. La deuxième dit qu’il est mis sur le plan illimité : la passerelle répond 403 à un compte sans plan.

Les deux suivantes sont des avertissements, pas des erreurs. La troisième dit que le port est ouvert et que rien ne chiffre cette liaison. Sur ce montage, l’appel de l’étape 14 ne quitte pas la machine. Dans un déploiement, la passerelle reçoit un certificat et écoute en TLS (livre blanc sécurité V3, section 6.2). La quatrième dit qu’elle ne présente aucun certificat client au moteur. Tant que la ligne [ÉCOUTE] annonce le port, la passerelle écoute. D’autres lignes d’annonce suivent, une par sous-système : exécution agentique, sortie réseau, documentation, gouvernance, délai de requête.

Si le démarrage s’arrête sur un message au lieu de ces lignes, lisez La passerelle refuse de démarrer.

Votre compte existe, il est sur un plan illimité, et il n’a encore le droit de rien. Ce sont deux questions distinctes : le plan dit ce que vous pouvez dépenser, le rôle dit ce que vous pouvez faire. La passerelle n’accorde aucun rôle d’office, pas même à admin.

Terminal A :

Fenêtre de terminal
cd llm-gateway
DATABASE_URL=postgresql://admin:admin_secret@localhost:6000/proxy \
npm run attribuer-un-role -- admin utilisateur-standard "tutoriel"

Résultat attendu : rôle « utilisateur-standard » attribué à « admin » par « tutoriel ».

Sans cette étape, l’appel de l’étape suivante reçoit Your identity is recognised, but no role you hold carries the right to call this endpoint. Ce refus nomme ce qui manque ; il ne s’agit pas d’un problème de clé.

Le troisième argument est l’auteur de l’attribution. Il est conservé avec elle : sur une installation réelle, savoir qui a donné un droit compte autant que savoir qui le détient.

Terminal A, celui où ADMIN_API_KEY est encore exportée :

Fenêtre de terminal
curl -s http://localhost:6001/code-principal/v1/chat/completions \
-H "authorization: Bearer $ADMIN_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"ignore","max_tokens":16,"messages":[{"role":"user","content":"Dis bonjour."}]}'

Résultat attendu : la réponse JSON du moteur, au format compatible OpenAI. Le texte du modèle est dans choices[0].message.content, et le champ usage porte les compteurs de jetons :

{
"id": "chatcmpl-678",
"object": "chat.completion",
"created": 1791223266,
"model": "qwen2.5-coder:14b",
"system_fingerprint": "fp_ollama",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Bonjour! Comment puis-je vous aider aujourd'hui?"
},
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 33, "completion_tokens": 12, "total_tokens": 45 }
}

Le premier appel attend que le moteur charge le modèle en mémoire : selon le matériel, la réponse peut mettre plus d’une minute à venir. Les appels suivants sont plus rapides.

Trois détails de cette commande se lisent. La passerelle retire le premier segment de l’URL, le nom de l’endpoint, et ajoute le reste, /v1/chat/completions, à l’adresse de base du moteur. Le champ model du corps vaut ignore : la passerelle le remplace par le modèle de l’endpoint, ici qwen2.5-coder:14b. L’endpoint décide du modèle, pas l’appelant. La clé de l’en-tête authorization est celle de votre compte sur la passerelle : la passerelle la vérifie, puis retire l’en-tête avant de relayer.

15. Constater que l’appel est passé par la passerelle

Section intitulée « 15. Constater que l’appel est passé par la passerelle »

Deux traces, indépendantes l’une de l’autre.

Regardez d’abord le terminal C : la passerelle a journalisé le passage.

[DEBUG] POST http://localhost:6001/code-principal/v1/chat/completions
[COST] admin: €0.000000 used (plan has no cap)
admin -> code-principal (qwen2.5-coder:14b)

Puis, dans le terminal A, relisez ce que la passerelle a inscrit en base :

Fenêtre de terminal
psql "$DATABASE_URL" -c "SELECT u.username, r.model, r.input_tokens, r.output_tokens, r.latency_ms FROM request_logs r JOIN users u ON u.id = r.user_id;"

Résultat attendu : une ligne, et une seule : admin, le modèle de l’endpoint, les deux compteurs de jetons rapportés par le moteur, et la durée de l’échange. Cette table porte l’attribution de chaque appel relayé à un compte.

Un appel adressé à la passerelle a été authentifié, rattaché à un compte, confronté aux droits de ce compte, relayé vers le moteur d’inférence sous le modèle que l’endpoint désigne, et inscrit en base à son nom. L’appelant n’a connu que l’adresse de la passerelle et le nom d’un endpoint : ni l’adresse du moteur, ni le nom du modèle. Le modèle est un modèle à poids ouverts servi sur la machine, et aucun appel n’en est sorti.

Vous avez aussi vu les points de configuration qui n’ont pas de valeur par défaut : DATABASE_URL et ADMIN_API_KEY bloquent le démarrage de la passerelle tant qu’elles sont absentes, et l’adresse du moteur se lit dans la base, pas dans le code.

Un poste de développement vise ce même endpoint, par la clé propre à son porteur. Le dialecte est celui de l’API compatible OpenAI :

- name: Code principal (passerelle)
provider: openai
model: any
apiBase: http://localhost:6001/code-principal/v1
apiKey: <la clé du porteur>

Pour arrêter le montage : Ctrl+C dans les terminaux B et C, puis docker compose down dans gateway-db, ou docker compose down -v pour effacer aussi les données.

Quand un démarrage s’arrête sur un message, ou quand la passerelle démarre mais que chaque requête échoue, la marche à suivre est dans La passerelle refuse de démarrer. Pour comprendre ce que le détour par une passerelle apporte, lisez Le rôle de la passerelle.

Pour déclarer le moteur d’inférence de votre périmètre à la place d’Ollama, avec son adresse en https://, suivez Déclarer un modèle servi par la passerelle. Le raccordement d’un poste est l’objet de Votre première session dans VS Code.