One Door to the Models, partie 3. L'abstraction de provider et le streaming
Avec stream: true, le comptage de tokens devient une estimation. En WebSocket, le load balancing disparaît. Partie 3 : les garanties par transport.

Les garanties de la gateway ne sont pas des propriétés de la gateway. Ce sont des propriétés du transport que l'appelant a choisi, et elles s'affaiblissent à chaque pas vers le temps réel. Un appel requête-réponse ordinaire est mesuré à partir du bloc usage que le modèle a réellement renvoyé. Le même appel avec stream: true voit ses prompt tokens estimés quoi que dise la politique, et ses completion tokens estimés eux aussi. Une connexion WebSocket ne peut pas du tout être répartie en load balancing entre plusieurs backends, parce qu'une fois établie elle est épinglée en un-pour-un entre client et backend. La partie 2 a construit l'infrastructure. Cette partie y pose une API, et la version honnête de cette API est celle qui dit à chaque appelant quelles garanties il vient d'abandonner.
Ce qu'une équipe consommatrice devrait avoir à savoir
Exactement trois choses : une URL de base, un identifiant client et un nom de modèle qui a du sens pour elle plutôt que pour Azure. Rien sur les déploiements, les régions, les providers, ni sur lequel des trois a changé mardi dernier. C'est tout le contrat, et tout ce que contient cette partie existe pour le garder vrai pendant que ce qu'il recouvre bouge.
L'unified model API d'API Management est construite exactement pour cela. Les clients parlent le format OpenAI Chat Completions, et la gateway traduit vers le format backend que le modèle cible utilise réellement. Elle supporte deux formats d'API backend, OpenAI Chat Completions et l'API Anthropic Messages, et elle fait quatre choses qui méritent d'être nommées séparément : standardiser le format côté client indépendamment des backends, appliquer un seul jeu de politiques de gouvernance à travers les providers, configurer le failover entre providers, et découpler les noms de modèles côté client des noms de modèles backend au moyen d'aliases.
L'alias est la pièce qui compte le plus ici, et c'est la continuation directe de la partie 2. Un déploiement de modèle épinglé sur gpt-4.1 version 2025-04-14 avec NoAutoUpgrade n'est utile que si aucune application ne le nomme jamais. Les applications appellent chat-default. L'alias fait correspondre chat-default à un déploiement, et le jour où cette correspondance change est un changement de configuration de gateway relu dans une pull request, pas une release coordonnée entre cinq équipes.
Deux réserves ont leur place ici plutôt que dans un ticket de support. L'unified model API est en préversion et en cours de déploiement ; dans les tiers classiques, l'accès anticipé passe par le canal AI Gateway Early release, un réglage de mise à jour par instance. Et ses deux formats backend supportés ne couvrent pas tout ce que l'entreprise exploite. Les déploiements Mistral et Llama dans Microsoft Foundry sont atteints par leur surface chat completions compatible OpenAI, que l'unified API peut exposer comme un backend au format OpenAI, mais c'est un chemin d'intégration différent de la traduction Anthropic native, et il vaut la peine de noter quel modèle arrive par quelle route avant que quelqu'un ne débogue un décalage de format à 2 heures du matin.
Routage : les groupes de priorité d'abord, les poids ensuite
Derrière un alias se trouve un backend pool, pas un backend. Les pools d'API Management supportent la distribution round-robin, pondérée, par priorité et session-aware, et le motif utile pour le trafic de modèles combine les deux premières : un groupe prioritaire de déploiements Provisioned Throughput qui doit absorber tout ce qu'il peut, et un groupe de priorité inférieure de déploiements pay-as-you-go qui ne voit du trafic que quand le premier groupe est indisponible. À l'intérieur d'un groupe, les poids répartissent la charge.
Les pools se configurent par le portail, l'API REST, ou un template ARM ou Bicep. La partie 2 a choisi Terraform, et c'est précisément le cas qu'elle décrivait comme légitime pour l'échappatoire azapi : une seule ressource, un corps ARM brut, posée à côté de ressources azurerm qu'elle référence par ID.
resource "azapi_resource" "chat_pool" {
type = "Microsoft.ApiManagement/service/backends@2023-09-01-preview"
name = "chat-pool"
parent_id = azurerm_api_management.gw.id
# azurerm has no backend-pool schema yet. Revisit when it does.
body = {
properties = {
description = "PTU first, PAYG overflow"
type = "Pool"
pool = {
services = [
{ id = azapi_resource.be_ptu_west.id, priority = "1", weight = "1" },
{ id = azapi_resource.be_payg_west.id, priority = "2", weight = "3" },
{ id = azapi_resource.be_payg_north.id, priority = "2", weight = "1" },
]
}
}
}
}
La priorité 1 est le déploiement PTU : il est déjà payé à l'heure, donc chaque token qu'il peut servir est un token qui ne coûte rien de plus. La priorité 2 ne reçoit du trafic que quand la priorité 1 ne peut pas le prendre, et le poids de 3 contre 1 entre deux régions reflète le quota plutôt qu'une préférence. C'est une topologie de coûts exprimée en routage, le genre de chose invisible sur un diagramme d'architecture et évident sur une facture.
Le circuit breaker, et jusqu'où lui faire confiance
Un membre de pool en échec devrait cesser de recevoir du trafic sans qu'un humain ait à le décider. API Management expose un circuit breaker comme propriété du backend : des règles définissent un nombre ou un pourcentage d'échecs dans un intervalle et les plages de codes de statut qui comptent comme échec, et quand le breaker se déclenche la gateway cesse d'appeler ce backend pendant la durée du déclenchement et renvoie 503 Service Unavailable au client. Si le backend a envoyé un header Retry-After, le breaker peut accepter cette valeur et attendre exactement ce délai, ce qui, pour un endpoint de modèle soumis à une limite de débit, vaut bien mieux qu'une supposition fixe.
circuitBreaker = {
rules = [{
name = "ptu-5xx"
failureCondition = {
count = 3
interval = "PT1H"
statusCodeRanges = [{ min = 500, max = 599 }]
}
tripDuration = "PT1H"
acceptRetryAfter = true
}]
}
Deux propriétés de cette fonctionnalité décident de la manière dont vous avez le droit d'en raisonner. D'abord, les règles de déclenchement sont approximatives : la gateway est distribuée, les instances ne synchronisent pas l'état du breaker, et chacune applique la règle sur l'information dont elle dispose. Un breaker configuré pour se déclencher après trois échecs peut en pratique en laisser passer nettement plus sur une instance scale-out. C'est un mécanisme de protection, pas un compteur, et aucun calcul de SLO ne devrait reposer sur son seuil exact. Ensuite, il n'est pas disponible dans le tier Consumption, une raison de plus pour laquelle la décision de tier de la partie 2 n'était pas une affaire de coût.
Le 503 mérite une conception délibérée. Un appelant qui reçoit 503 de la gateway ne peut pas savoir si le modèle est tombé, si le breaker est ouvert ou si l'instance entière est en mauvaise santé, et les trois cas appellent des comportements client différents. Ajouter dans la politique outbound un header de réponse qui nomme le backend essayé, et journaliser l'état du breaker, transforme un 503 opaque en quelque chose qu'une équipe consommatrice peut traiter sans ouvrir de ticket.
Le streaming, et la comptabilité qu'il vous coûte
Toutes les équipes consommatrices demandent le streaming, et elles ont raison. Le temps jusqu'au premier token est le nombre qu'un humain perçoit ; la latence totale est le nombre qu'un dashboard affiche. Les server-sent events sur le même endpoint HTTP sont la réponse standard, le client positionne stream: true, et rien ne change dans le routage de la gateway.
Ce qui change, c'est le compteur. La politique de limite de tokens travaille normalement sur l'usage réel : avec estimate-prompt-tokens="false" elle lit la section usage de la réponse du modèle, ce qui signifie qu'une requête peut dépasser la limite et n'être détectée qu'après coup, bloquant les requêtes suivantes jusqu'à la remise à zéro de la fenêtre. Avec l'estimation activée, elle compte les prompt tokens à partir du schéma de la définition d'API avant l'appel, échangeant un peu de précision contre le fait de ne pas brûler du quota backend pour découvrir le dépassement.
Le streaming supprime le choix. Quand stream: true est positionné, les prompt tokens sont toujours estimés quel que soit le réglage de la politique, et les completion tokens sont estimés aussi. Aucune configuration ne permet de mesurer une requête streamée à partir de la vérité terrain à la gateway. Pour les modèles qui acceptent des images, la dérive se cumule : avec le streaming activé, ou avec l'estimation active, la politique surcompte les tokens d'image au lieu de prendre le décompte du backend.
Ce n'est pas une raison de refuser le streaming. C'est une raison d'être précis sur ce que les nombres veulent dire, parce que la partie 5 les transforme en argent :
- HTTP non streamé : mesuré à partir du
usagedu modèle lui-même. Exact. - SSE streamé : prompt et completion tokens estimés à la gateway. Suffisant pour la limitation de débit, approximatif pour la facturation.
- WebSocket : voir plus bas. En pratique hors bande.
La réponse du control plane est la réconciliation plutôt qu'une meilleure estimation. Les métriques de la gateway restent le signal temps réel qui fait respecter les quotas, et le job mensuel d'attribution que construit la partie 5 corrige contre l'usage faisant autorité que le provider rapporte, avec le delta suivi comme métrique à part entière. Une dérive qui grandit est un bug ; une dérive qui reste stable à quelques pour cent est le coût du streaming, et elle doit être inscrite dans le document de chargeback plutôt que découverte par un product owner.
Les WebSockets, où l'abstraction s'arrête
Le trafic voix temps réel et speech-to-speech ne rentre pas dans le modèle requête-réponse, et API Management supporte les API WebSocket. Les limitations sont assez tranchantes pour changer l'architecture plutôt que la décorer.
Les connexions WebSocket ne peuvent être ni distribuées ni réparties en load balancing entre plusieurs backends. Une fois établie, chaque connexion est maintenue en un-pour-un entre client et backend. Chaque décision de routage de ce billet, les groupes de priorité, la topologie PTU d'abord, la répartition régionale pondérée, s'applique au handshake initial et à rien après lui. Un backend qui se dégrade en pleine session ne peut pas être basculé par la gateway ; la session doit se terminer et le client doit se reconnecter. Tout client sur ce chemin a besoin d'une logique de reconnexion avec backoff, et tout modèle de capacité doit traiter une session WebSocket comme une réservation épinglée plutôt que comme un flux de requêtes routables indépendamment.
Trois contraintes plus petites suivent. Une longue liste de politiques ne peut pas s'appliquer à l'opération onHandshake, dont le caching, CORS, la manipulation du corps et toutes les politiques validate-*, et les politiques héritées d'un scope global ou produit sont sautées à l'exécution au lieu d'échouer bruyamment. La politique set-header ne peut pas modifier certains headers bien connus, dont Host, sur les requêtes de handshake. Et la validation TLS est plus stricte que pour les API HTTP : pour un backend WebSocket, la gateway vérifie que le certificat serveur est de confiance et que son subject name correspond au hostname, là où une API HTTP n'exige que la confiance. Un backend dont le subject de certificat ne correspond pas et qui fonctionne depuis des années en HTTP échouera dès qu'il sera exposé comme API WebSocket.
La conséquence pratique : le chemin temps réel reçoit sa propre API, son propre produit et son propre quota, et la documentation consommateur dit clairement qu'il n'est pas couvert par les garanties de failover de l'endpoint de chat. Prétendre le contraire, c'est ainsi qu'une revue d'incident découvre la différence.
La session affinity, et pourquoi éviter d'en avoir besoin
Les pools supportent la session awareness au moyen d'un cookie, le client stockant une valeur Set-Cookie et la renvoyant pour que les requêtes suivantes atteignent le même backend. Cela existe pour les API stateful côté serveur, l'Assistants API en étant l'exemple canonique, où le client doit garder un session ID, extraire un thread ID du corps de la réponse et envoyer le bon cookie au bon appel.
Cela fonctionne, et c'est une contrainte qui mérite d'être évitée dès la conception. Un appel lié par session affinity est un appel qui ne peut pas être rerouté, ce qui le place dans la même catégorie que le chemin WebSocket : pas de failover, et un modèle de capacité où la charge d'un backend dépend des sessions qui y ont atterri plutôt que du nombre de requêtes arrivées. Quand une API stateful est réellement nécessaire, gardez-la sur un alias séparé pour que ses garanties plus faibles ne soient pas héritées en silence par le trafic stateless qui constitue le reste du volume.
Modes de défaillance à surveiller
- Le 503 qui veut dire quatre choses différentes. Breaker ouvert, tous les membres du pool épuisés, gateway en mauvaise santé, ou backend réellement défaillant. Sans un header ou une ligne de log qui nomme lequel, chacun des quatre devient un ticket.
- La dérive du streaming traitée comme un bug. C'est un comportement documenté. La défaillance n'est pas la dérive, c'est publier un nombre estimé à un endroit qui dit facture.
- Une fonctionnalité en préversion dans le chemin critique. L'unified model API est en préversion. C'est acceptable pour la couche de traduction seulement si le repli, une API directe par provider derrière le même produit et les mêmes politiques, existe déjà et est testé, au lieu d'être conçu pendant l'incident.
- Des politiques sautées en silence sur les API WebSocket. Les politiques héritées non supportées sur
onHandshakesont sautées à l'exécution. Un contrôle de sécurité supposé global ne l'est pas ici, et rien ne vous le dira.
Ce dont hérite la partie 4
À la fin de cette partie, une équipe consommatrice a un endpoint, un format et des aliases de modèles stables, avec le routage et le failover derrière eux et un énoncé documenté des garanties qui s'appliquent à chaque transport. Ce que rien de tout cela ne gère, c'est le travail qui n'a pas besoin d'une réponse maintenant : la synthèse nocturne des tickets de support qui, sinon, occuperait le même chemin synchrone qu'un client qui attend un premier token. C'est une file d'attente, et c'est la suite.
À lire ensuite
- Partie 4, le travail asynchrone hors du chemin de requête, où le travail batch et en queue quitte le chemin synchrone, et où la batch API se révèle être une seconde porte sans aucune politique devant elle.
- Le streaming des réponses est une décision d'UX, pas de performance, l'autre moitié de l'argument du streaming : ce qu'il apporte à la personne qui attend.
- Applications LLM multi-tenant : isoler les clients sur un modèle partagé, la question de tenancy à laquelle cette gateway répond par des produits et des quotas plutôt que par des stacks séparées.
Pour le volet infrastructure et plateforme de l'exploitation de tout cela à grande échelle, les notes de terrain sont sur ercan.cloud, et le hub se trouve sur ercanermis.com.
Références
Plus d'Ercan
Deux autres sites, même auteur, terrain différent.
Cloud, AWS, EKS, Terraform, plateforme.
Notes de terrain de systèmes de production. EKS, IAM, Terraform à l'échelle organisation, observabilité, optimisation des coûts.
Visiter ercan.cloud →Le hub. À propos, conseil, contact.
Hub personnel pour les deux pistes d'écriture. Qui je suis, comment fonctionne le conseil, comment me joindre.
Visiter ercanermis.com →