Version 1.0.0
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.
1. Donner ses identifiants à la base
Section intitulée « 1. Donner ses identifiants à la base »Terminal A. gateway-db/docker-compose.yml lit un fichier .env qui n’est pas versionné.
Seul l’exemple l’est :
cd gateway-dbcp .env.example .envRé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.
2. Lever la base
Section intitulée « 2. Lever la base »Toujours dans le terminal A, dans gateway-db :
docker compose up -d --waitRé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.
3. Jalonner le registre des migrations
Section intitulée « 3. Jalonner le registre des migrations »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 :
export DATABASE_URL=postgresql://admin:admin_secret@localhost:6000/proxy./migrate.sh --baseline 029Ré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.
4. Appliquer les migrations en attente
Section intitulée « 4. Appliquer les migrations en attente »./migrate.shRésultat attendu : base à jour, aucune migration à appliquer. À partir d’ici, cette
commande seule applique les migrations écrites après celles-ci.
5. Engendrer la clé d’administration
Section intitulée « 5. Engendrer la clé d’administration »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.
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.
6. Construire la console d’administration
Section intitulée « 6. Construire la console d’administration »Terminal B, à la racine du dépôt :
cd gateway-adminnpm installLEMNISCATE_DEPLOYMENT_PROFILE=serverless npm run buildRé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.
7. Lever la console d’administration
Section intitulée « 7. Lever la console d’administration »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.
DATABASE_URL=postgresql://lemniscate_console:console_secret@localhost:6000/proxy \ ADMIN_USER=admin ADMIN_PASS=change-me npm run startRé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.
8. Déclarer le moteur d’inférence
Section intitulée « 8. Déclarer le moteur d’inférence »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 :
curl -s http://127.0.0.1:11434/v1/modelsRé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.
9. Déclarer le modèle
Section intitulée « 9. Déclarer le modèle »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.
10. Créer l’endpoint
Section intitulée « 10. Créer l’endpoint »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.
11. Installer et construire la passerelle
Section intitulée « 11. Installer et construire la passerelle »Terminal C, à la racine du dépôt :
cd llm-gatewaynpm installLEMNISCATE_DEPLOYMENT_PROFILE=serverless npm run buildRé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.
12. Lever la passerelle
Section intitulée « 12. Lever la passerelle »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.
DATABASE_URL=postgresql://lemniscate_gateway:gateway_secret@localhost:6000/proxy \ ADMIN_API_KEY=<la clé affichée à l'étape 5> \ npm run startRé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.
13. Donner à votre compte le droit d’appeler
Section intitulée « 13. Donner à votre compte le droit d’appeler »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 :
cd llm-gatewayDATABASE_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.
14. Router un appel par la passerelle
Section intitulée « 14. Router un appel par la passerelle »Terminal A, celui où ADMIN_API_KEY est encore exportée :
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 :
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.
Ce que vous avez établi
Section intitulée « Ce que vous avez établi »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.