Brancher plusieurs providers — environnements et matrice d’usages
Cinquieme notebook de la serie « AI Engine par son API ». AI Engine supporte neuf providers distants et un connecteur pour moteurs auto-heberges (compatible OpenAI). Mais ou vivent ces branchements, et comment les manipuler par l’API ? Ce notebook ouvre la regie des environnements : lire la configuration, interroger un provider, declarer un second environnement, basculer l’usage chat de l’un a l’autre — et retablir l’etat initial a la fin.
La route la plus innocente du catalogue cache le piege le plus destructeur de la serie : settings/update ne met pas a jour, il remplace. Ce notebook le demontre en securite (instantane complet en memoire, restauration immediate), parce que la demonstration a eu lieu par accident pendant le sondage — et qu’elle a failli couter l’instance.
Les sorties de ce notebook proviennent d’une execution reelle contre l’instance locale (voir « Provenance et limites », en fin de fichier). Aucune cle, aucune adresse de provider n’y figure : les champs sensibles sont masques par code, jamais supprimes a la main.
La serie « AI Engine par son API »
Le projet Livres Agites a mis AI Engine au coeur d’une maison d’edition : bot d’accueil, agents d’ateliers, bibliothecaire documentee par RAG, formulaires dynamiques. Cette serie presente le plugin de maniere reproductible — sans jamais exposer de donnees client :
Notebook
Contenu
presenter-ai-engine-par-son-api
instance, API, catalogue des chatbots, premiere completion
configurer-chatbots-par-l-api
lire, dupliquer, ecrire et interroger des chatbots (document JSON global)
administrer-les-formulaires-par-l-api
le formulaire comme contenu : CRUD, publication, rendu public
piloter-wordpress-par-mcp
le serveur MCP : handshake, catalogue d’outils, appels reels
environnements, matrice d’usages, multi-provider par l’API
notebooks suivants
agents MCP distants, WooCommerce metier, …
Trois niveaux de lecture, dans chaque notebook :
Decouverte — ce que fait la fonctionnalite, vue par l’API ;
Branchement — comment on l’a branchee dans le projet ;
Exercice — reutiliser le pattern sur un cas voisin.
# Configuration et helpers. Aucune cle ni adresse de provider n'est stockee# dans ce fichier : tout vient de instance-jetable/.env (README, etape 5).import base64import copyimport jsonimport osfrom pathlib import Pathimport requestsfrom dotenv import load_dotenv# Localisation du .env : a cote du notebook (instance-jetable/.env),# sinon dans le repertoire courant.charges = []for candidat in (Path("instance-jetable/.env"), Path(".env")):if candidat.exists(): load_dotenv(candidat) charges.append(str(candidat))print("Fichiers .env charges :", charges or"(aucun)")BASE_URL = os.getenv("VALMONT_BASE_URL", "http://localhost:8093").rstrip("/")ADMIN_USER = os.getenv("VALMONT_ADMIN_USER", "")APP_PASSWORD = os.getenv("VALMONT_APP_PASSWORD", "")print("Base URL :", BASE_URL)creds = base64.b64encode(f"{ADMIN_USER}:{APP_PASSWORD}".encode()).decode()ENTETES = {"Authorization": "Basic "+ creds, "Content-Type": "application/json"}def api(route, method="GET", payload=None):"""Appel a l'API REST d'administration de AI Engine (mwai/v1).""" r = requests.request(method, BASE_URL +"/wp-json"+ route, headers=ENTETES, json=payload, timeout=120) r.raise_for_status()return r.json()def modeles_de(env):"""Les identifiants de modeles declares dans un environnement."""return [m["model"] ifisinstance(m, dict) else mfor m in (env.get("models") or [])]
Fichiers .env charges : ['instance-jetable\\.env']
Base URL : http://localhost:8093
Un WordPress, des environnements, une matrice d’usages
La configuration de AI Engine est une seule option WordPress (mwai_options), lisible par GET /mwai/v1/settings/options. Elle contient tout : les environnements, les modules, les limites. Deux familles nous interessent ici :
ai_envs — les environnements de modeles : chaque entree declare un provider (type openai, anthropic, … ou custom pour tout serveur compatible OpenAI : Ollama, vLLM, LM Studio), avec son endpoint, sa cle, et ses modeles.
la matrice d’usages — un jeu de cles ai_<usage>_default_env et ai_<usage>_default_model pour chaque usage : chat, fast, vision, images, audio, json, embeddings. Chaque usage route vers un environnement distinct. C’est le multi-provider du plugin : pas une liste de modeles, une matrice — le chat peut vivre sur un moteur local pendant que la vision vit sur un provider distant.
# 1. Lire la matrice : environnements et usagesoptions = api("/mwai/v1/settings/options")["options"]envs = options.get("ai_envs", [])print("Environnements de modeles (ai_envs) :", len(envs))for e in envs:print(f" {e['id']:14s} [{e.get('type'):7s}] {e.get('name')} : {modeles_de(e) or'(aucun modele declare)'}")embed_envs = options.get("embeddings_envs", [])print()print("Environnements d'embeddings (embeddings_envs) :", len(embed_envs))for e in embed_envs:print(f" [{e.get('type'):20s}] {e.get('name')}")USAGES = [ ("chat", "ai_default"), ("fast", "ai_fast_default"), ("vision", "ai_vision_default"), ("images", "ai_images_default"), ("audio", "ai_audio_default"), ("json", "ai_json_default"), ("embeddings", "ai_embeddings_default"),]print()print(f"{'usage':12s}{'environnement':16s} modele")for nom, prefixe in USAGES: env = options.get(prefixe +"_env") or"(defaut plugin)" model = options.get(prefixe +"_model") or"-"print(f"{nom:12s}{env:16s}{model}")
Un seul environnement est reellement branche : vllm-local (type custom, le moteur auto-heberge de l’instance), et il ne sert que l’usage chat. Tous les autres usages affichent (defaut plugin) : aucun environnement ne leur est assigne, et le modele affiche (gpt-5-mini, whisper-1, …) est la valeur de repli interne du plugin — pas un branchement reel. Si l’on appelait l’usage images sans avoir declare d’environnement pour OpenAI, l’appel echouerait faute de cle.
Les environnements d’embeddings, eux, sont predeclares par le plugin (la base WordPress interne, le vector store OpenAI) : la structure existe, mais aucune route REST de l’API gratuite ne permet d’y indexer ou d’y chercher — la regie du RAG passe par l’interface d’administration ou la version Pro.
C’est la photographie typique d’une installation modeste : un moteur local pour le chat, rien d’autre. Le reste de ce notebook montre ce que cette matrice devient quand on la manipule par l’API.
Interroger un provider : catalogue et poignee de main
Deux routes parlent aux environnements (et pas seulement d’eux) :
POST /ai/models (parametre envId) demande au provider la liste de ses modeles — la fiche technique : fonctionnalites, prix, contexte maximal. Pour un moteur auto-heberge, le prix est nul et la fiche est ce que le serveur declare.
POST /ai/test_connection (parametre env_id — oui, le nommage differe de la route precedente pour la meme notion ; le plugin n’est pas coherent ici) fait une vraie requete au serveur de modeles et rapporte ce qu’il a trouve.
# 2. Le catalogue du provider : ai/modelscatalogue = api("/mwai/v1/ai/models", method="POST", payload={"envId": "vllm-local"})["models"]print("modeles servis par vllm-local :", len(catalogue))for m in catalogue:print(f" {m['model']}")print(f" fonctionnalites : {m.get('features')}")print(f" contexte max : {m.get('maxContextualTokens')} tokens | completion max : {m.get('maxCompletionTokens')}")print(f" prix : {m.get('price')}{m.get('type')}/unite | tags : {m.get('tags')}")
modeles servis par vllm-local : 1
qwen3.6-35b-a3b
fonctionnalites : ['completion']
contexte max : 8192 tokens | completion max : 4096
prix : {'in': 0, 'out': 0} token/unite | tags : ['core', 'chat']
Lecture — un catalogue qui tient en un modèle
ai/models renvoie un seul modèle pour vllm-local : qwen3.6-35b-a3b. Trois champs méritent lecture :
features = ['completion'] : ce modèle est déclaré pour compléter, pas (via cette route) pour dialoguer. Le catalogue expose la capacité ; le point d’entrée ai/chat en fera l’usage, la cellule suivante serrera la main dans l’autre sens.
8192 tokens de contexte, 4096 de complétion max : le volume est borné — c’est l’ordre de grandeur d’une inférence locale quantifiée. C’est le guide de dimensionnement pour ne pas lui envoyer un corpus entier.
price = {in: 0, out: 0} et tags ['core', 'chat'] : l’instance de dev est gratuite et marquée core. Le prix nul ne dit rien de la valeur, il dit l’absence de comptage.
Le catalogue est donc la source de vérité sur ce que le serveur sait servir — on l’interroge avant de brancher un usage.
# 3. La poignee de main : ai/test_connectiontest = api("/mwai/v1/ai/test_connection", method="POST", payload={"env_id": "vllm-local"})details = (test.get("data") or {}).get("details") or {}print("vllm-local :", test.get("success"), "| provider :", test.get("provider"))print(" modeles trouves :", details.get("model_count"), details.get("sample_models"))# Le champ details['endpoint'] contient l'adresse du serveur de modeles :# un secret local, masque ici — on n'affiche que ce qu'il a trouve.inconnu = api("/mwai/v1/ai/test_connection", method="POST", payload={"env_id": "environnement-inexistant"})print()print("environnement inexistant :", inconnu.get("success"), "|", inconnu.get("error"))
ai/test_connection répond pour vllm-local : success = True, provider = custom, modeles trouves = 1 ['qwen3.6-35b-a3b']. Le provider est custom ; le champ details['endpoint'] — l’adresse du serveur de modèles — est volontairement masqué, c’est un secret local.
Le second appel répond face à un envId inventé : success = False, message « Environment not found. » Le serveur valide l’identifiant, il ne l’invente pas : une faute de frappe dans un env_id ne produit pas un modèle vide, elle produit un échec explicite. C’est le contrat à garder pour la suite — on peut donc interroger un environnement inexistant en toute sécurité : l’API refuse avant de toucher quoi que ce soit.
Le piege : settings/update ne met pas a jour, il remplace
Pendant le sondage preparatoire a ce notebook, un appel d’apparence innocente a ete emis : POST /settings/update avec un bloc d’une seule cle (ai_default_env, reprenant sa valeur actuelle). Verifie par la suite dans le code du plugin, le comportement est sans appel :
update_options fait un update_optiontotal : l’option WordPress complete est ecrasee par le seul bloc envoye ;
a la relecture, le plugin regenere les valeurs manquantes — par defaut : un environnement OpenAI vide, un nouvel identifiant aleatoire, les modules desactives, les compteurs d’usage perdus.
Bilan mesure sur l’instance : l’environnement vllm-local (endpoint, cle, modele) avait disparu ; ai_default_env pointait vers l’environnement OpenAI regenere ; module_embeddings etait repasse a false. Une seule cle envoyee, toute la regie detruite.
La demonstration ci-dessous est securisee : l’instantane complet de la configuration est d’abord charge en memoire (variables Python), le piege est declenche, les degats mesures, puis l’instantane est renvoye en bloc — la restauration exacte. Tout se joue dans une seule cellule : meme un arret du notebook entre le piege et la reparation laisserait l’instance recuperable en rejouant la cellule.
# 4. Le piege demontre en securite : instantane -> update partiel -> degats -> restaurationinstantane = api("/mwai/v1/settings/options")["options"]envs_avant = [e["id"] for e in instantane.get("ai_envs", [])]print("instantane :", len(instantane), "cles | envs :", envs_avant)# Le piege : un update PARTIEL, d'apparence inoffensive# (une seule cle, avec la valeur qu'elle a deja).piege_payload = {"module_statistics": instantane.get("module_statistics")}api("/mwai/v1/settings/update", method="POST", payload={"options": piege_payload})apres = api("/mwai/v1/settings/options")["options"]print()print("apres l'update partiel :")print(" nb de cles :", len(instantane), "->", len(apres))print(" env vllm-local :", any(e["id"] =="vllm-local"for e in apres.get("ai_envs", [])))print(" envs restants :", [(e["id"], e.get("type")) for e in apres.get("ai_envs", [])])print(" ai_default_env :", instantane.get("ai_default_env"), "->", apres.get("ai_default_env"))print(" ai_default_model :", instantane.get("ai_default_model"), "->", apres.get("ai_default_model"))print(" module_embeddings :", instantane.get("module_embeddings"), "->", apres.get("module_embeddings"))print(" compteurs d'usage :", "ai_usage"in instantane, "->", "ai_usage"in apres)# La restauration : renvoyer le bloc COMPLET. L'instantane en memoire# a survecu au carnage — c'est le seul exemplaire de la configuration.retour = api("/mwai/v1/settings/update", method="POST", payload={"options": instantane})final = api("/mwai/v1/settings/options")["options"]print()print("apres restauration :")print(" nb de cles :", len(final))print(" env vllm-local :", any(e["id"] =="vllm-local"for e in final.get("ai_envs", [])))print(" ai_default_env :", final.get("ai_default_env"))print(" module_embeddings :", final.get("module_embeddings"))
Pourquoi le read-modify-write complet, et pas autre chose
La regle du grain 2 (chatbots) etait deja : lire la liste, la modifier, renvoyer la liste entiere. Ici elle devient une condition de survie. Deux nuances comptent :
l’instantane vit en memoire Python, pas dans le WordPress : quand l’option est ecrasee, l’exemplaire Python survit — c’est lui qu’on renvoie pour restaurer ;
la relecture regenere des valeurs : les identifiants aleatoires de l’environnement fantome changeront d’une execution a l’autre, et le compte de cles peut varier selon les compteurs d’usage — les sorties ci-dessus mesurent la structure du degat (environnement detruit, defauts regeneres), pas des valeurs litterales stables.
En production, la parade est la meme qu’au grain 2, une echelle au-dessus : toujours relire, toujours renvoyer le bloc complet, jamais presumer d’un merge cote serveur. Une route qui s’appelle update et un contrat qui est un PUT — c’est le genre d’ecart qu’on ne devine pas, qu’on mesure.
Ecrire le multi-provider : declarer, basculer, retablir
Le cycle complet, sur la matrice : cloner l’environnement local en un second (vllm-secours — meme endpoint, autre identite administrative), interroger le clone (meme serveur, meme catalogue), basculer l’usage chat vers lui, puis remettre et purger. L’instance finit exactement comme elle a commence — chaque ecriture envoie le bloc complet, comme la section precedente l’impose.
Ce cycle est le squelette de toute migration de provider : declarer le nouveau moteur a cote de l’ancien, verifier qu’il repond, basculer un usage, verifier encore, et garder l’ancien a portee de main pour revenir. Rien ici ne demande d’interface d’administration.
# 5. Declarer un second environnement : vllm-secoursinstantane = api("/mwai/v1/settings/options")["options"]envs = copy.deepcopy(instantane["ai_envs"])source =next(e for e in envs if e["id"] =="vllm-local")secours = copy.deepcopy(source)secours["id"] ="vllm-secours"secours["name"] ="Valmont vLLM secours"envs.append(secours)instantane["ai_envs"] = envsrep = api("/mwai/v1/settings/update", method="POST", payload={"options": instantane})verif = api("/mwai/v1/settings/options")["options"]print("update :", rep.get("success"), "|", rep.get("message"))for e in verif["ai_envs"]:print(f" {e['id']:14s} [{e.get('type'):7s}] {modeles_de(e)}")
Lecture — déclarer un second environnement par recopie
Ajouter vllm-secours ne part pas de zéro : la cellule recopie la configuration de vllm-local (copy.deepcopy(source)), change id et name, puis re-POST le settings/update complet. Résultat : update = True, et ai_envs porte désormais deux entrées — vllm-local et vllm-secours, même type custom, même modèle qwen3.6-35b-a3b.
Le geste est un read-modify-write : on lit ai_envs, on le modifie en mémoire, on le réécrit en entier. C’est précisément la contre-mesure du piège de la section précédente — settings/updateremplace, il ne patch pas, donc il faut toujours renvoyer l’objet complet. L’id suffit à faire un nouvel environnement : le serveur n’exige aucun enregistrement préalable.
# 6. Interroger le nouveau venu : meme serveur, autre environnementcatalogue = api("/mwai/v1/ai/models", method="POST", payload={"envId": "vllm-secours"})["models"]print("catalogue du secours :", [m["model"] for m in catalogue])test = api("/mwai/v1/ai/test_connection", method="POST", payload={"env_id": "vllm-secours"})print("connexion du secours :", test.get("success"),"|", (test.get("data") or {}).get("message"))
catalogue du secours : ['qwen3.6-35b-a3b']
connexion du secours : True | Connection successful. Found 1 models.
Lecture — le secours répond : même serveur, autre enveloppe
Le nouveau envId est interrogé deux fois. ai/models sur vllm-secours → ['qwen3.6-35b-a3b'] ; ai/test_connection sur vllm-secours → True, « Connection successful. Found 1 models. »
Le secours tient le même modèle que l’instance principale — un seul, toujours qwen3.6-35b-a3b. C’est cohérent : vllm-secours est une copie de vllm-local, donc les deux pointent le même backend sous un id différent. La valeur de la déclaration n’est pas un nouveau moteur, c’est une enveloppe de repli : si vllm-local tombe, vllm-secours est déjà configuré. La suite montre comment basculer.
La bascule réécrit ai_default_env en read-modify-write : vllm-local → vllm-secours. La vérification rend default_env bascule : vllm-secours | model : qwen3.6-35b-a3b, puis la cellule remet tout à sa place : default_env remis : vllm-local.
Deux lectures : (1) ai_default_model suit l’environnement — basculer l’env change aussi le modèle par défaut, on ne les règle pas indépendamment. (2) La séquence est une inversion maîtrisée : on écrit, on vérifie, on remet. C’est l’ombre d’un blue-green, et le test de non-régression est justement remis = vllm-local — l’usage est revenu à son point de départ.
# 8. Purger le secours : l'instance revient a son etat initialinstantane = api("/mwai/v1/settings/options")["options"]instantane["ai_envs"] = [e for e in instantane["ai_envs"] if e["id"] !="vllm-secours"]api("/mwai/v1/settings/update", method="POST", payload={"options": instantane})final = api("/mwai/v1/settings/options")["options"]print("envs final :", [e["id"] for e in final["ai_envs"]])print("default_env :", final.get("ai_default_env"), "| model :", final.get("ai_default_model"))print("module_embeddings :", final.get("module_embeddings"))test = api("/mwai/v1/ai/test_connection", method="POST", payload={"env_id": "vllm-local"})print("connexion vllm-local :", test.get("success"),"|", (test.get("data") or {}).get("message"))
envs final : ['vllm-local']
default_env : vllm-local | model : qwen3.6-35b-a3b
module_embeddings : True
connexion vllm-local : True | Connection successful. Found 1 models.
Lecture — purger : la réversibilité du read-modify-write
La purge réécrit ai_envssansvllm-secours ([e for e in ... if e["id"] != "vllm-secours"]). L’état final est bien l’état initial : envs final = ['vllm-local'], default_env = vllm-local, model = qwen3.6-35b-a3b, module_embeddings = True, et ai/test_connection sur vllm-local → True, « Connection successful. Found 1 models. »
C’est la leçon de la section : parce que settings/updateremplace l’objet entier, toute modification (ajout, bascule, purge) est entièrement réversible — il suffit de réécrire ai_envs (ou ai_default_env) avec la valeur voulue. L’instance jetable revient à son état de référence sans état résiduel, et la dernière poignée de main confirme que le serveur est de nouveau opérationnel.
Ce qu’on en a fait dans le projet Livres Agites
Dans le projet d’origine, la matrice d’usages est restee volontairement simple cote chat : un seul environnement custom, le moteur auto-heberge — pas de dependance a un provider distant, pas de cle qui expire, un cout marginal nul. La richesse multi-provider du plugin s’est logee ailleurs : cote embeddings, ou la regie separe les environnements de vecteurs par regime d’acces (le Parcours 2 de livresagites-parcours.md detaille le piege du multi-environnement de vecteurs et son compagnon separer-les-environnements-de-vecteurs.ipynb).
Les frontieres mesurees sur la version gratuite, a garder en tete :
neuf providers distants declarables, aucun branchable sans cle : la structure multi-provider est entierement presente dans l’API, mais chaque environnement distant exige ses credentials — rien a demontrer sans cle ;
aucune route REST pour le RAG : les embeddings_envs se lisent et s’ecrivent comme ici, mais l’indexation et la recherche de vecteurs ne passent pas par l’API gratuite — la regie du vector store est le domaine de l’interface d’administration et de la version Pro.
La lecon transposable reste celle de la section precedente : une API d’administration qui semble granulaire (une route par cle) peut cacher un contrat total (un PUT par bloc). On ne le sait qu’en mesurant — ou en lisant le code, qui est la version silencieuse de la mesure.
Exercices
Trois exercices, du plus simple au plus integre. Les fonctions sont a completer ; api() et modeles_de() sont disponibles. Chaque exercice se verifie d’une ligne de test.
Exercice 1 — comparer deux catalogues
Completez comparer_catalogues(env_a, env_b) : elle interroge /ai/models pour deux environnements et retourne un dict avec les modeles communs et les modeles propres a chacun.
def comparer_catalogues(env_a, env_b):"""Compare les catalogues de deux environnements. Retourne {"communs": [...], "seuls_a": [...], "seuls_b": [...]}. """# A COMPLETER : deux appels POST /mwai/v1/ai/models (parametre envId),# puis intersection et differences sur les identifiants de modeles.return {}
Exercice 2 — la matrice d’usages complete
Completez matrice_usages() : elle lit settings/options et retourne la matrice {usage: {"env": ..., "model": ...}} pour les sept usages du notebook (chat, fast, vision, images, audio, json, embeddings) — les usages non branches devant apparaitre avec env: None.
def matrice_usages():"""Retourne la matrice {usage: {"env": ..., "model": ...}}."""# A COMPLETER : GET settings/options, extraire les couples# ai_<usage>_default_env / ai_<usage>_default_model.return {}
Exercice 3 — basculer proprement
Completez basculer_provisoirement(env_id) : elle applique le pattern du notebook a un changement d’environnement par defaut — instantane complet, bascule, verification, restauration, verification finale — et retourne True seulement si l’etat final est identique a l’etat initial. L’exercice est rate si la configuration ne revient pas exactement.
def basculer_provisoirement(env_id):"""Bascule ai_default_env vers env_id puis retablit ; True si propre."""# A COMPLETER : instantane, update complet avec la bascule, relecture,# update complet de restauration, relecture, comparaison.returnNone
Provenance et limites
Instance testee : http://localhost:8093, montee via instance-jetable/docker-compose.jetable.example.yml, AI Engine 3.7.0 (version gratuite, wordpress.org), corpus synthetique « Maison Valmont ».
Determinisme : aucune completion LLM — tout l’arc est catalogues, poignees de main et configuration. Les valeurs structurelles (nombre d’environnements, modeles servis, succes des connexions) sont stables ; les identifiants aleatoires regeneres par le piege et le compte exact de cles varient d’une execution a l’autre — les sorties mesurent la structure, pas ces litteraux.
Endpoints verifies ici (firsthand) : GET /mwai/v1/settings/options, POST /mwai/v1/settings/update (x6 : piege, restauration, clone, bascule, remise, purge), POST /mwai/v1/ai/models (x2), POST /mwai/v1/ai/test_connection (x4, dont un environnement inexistant).
Incident reel : la section « le piege » decrit un accident survenu pendant le sondage preparatoire — l’instance a ete restauree par le meme mecanisme (instantane complet renvoye en bloc) que celui demontre dans le notebook.
Secrets : aucun endpoint, aucune cle n’est affiche ; le champ details['endpoint'] de test_connection est masque par code (il contient l’adresse locale du serveur de modeles).
Frontieres : pas de donnees client, pas de secret, pas d’IP de provider — regles du chantier CoursIA.