Die Garantien des Gateways sind keine Eigenschaften des Gateways. Sie sind Eigenschaften des Transports, den der Aufrufer gewählt hat, und sie werden mit jedem Schritt Richtung Echtzeit schwächer. Ein gewöhnlicher Request-Response-Aufruf wird aus dem usage-Block gemessen, den das Modell tatsächlich zurückgegeben hat. Derselbe Aufruf mit stream: true bekommt seine Prompt-Tokens geschätzt, egal was die Policy sagt, und seine Completion-Tokens ebenfalls. Eine WebSocket-Verbindung lässt sich überhaupt nicht über Backends load balancen, denn einmal aufgebaut ist sie eins-zu-eins zwischen Client und Backend gepinnt. Teil 2 hat die Infrastruktur gebaut. Dieser Teil setzt eine API darauf, und die ehrliche Version dieser API ist eine, die jedem Aufrufer sagt, welche Garantien er gerade aufgegeben hat.

Was ein Consumer-Team wissen müssen sollte

Genau drei Dinge: eine Base URL, ein Client-Credential und einen Modellnamen, der für das Team etwas bedeutet statt für Azure. Nichts über Deployments, Regionen, Provider oder darüber, was davon sich letzten Dienstag geändert hat. Das ist der ganze Vertrag, und alles in diesem Teil existiert, um ihn wahr zu halten, während sich die Dinge dahinter bewegen.

Die unified model API von API Management ist genau dafür gebaut. Clients sprechen das OpenAI-Chat-Completions-Format, und das Gateway übersetzt in das Backend-Format, das das Zielmodell tatsächlich verwendet. Sie unterstützt zwei Backend-API-Formate, OpenAI Chat Completions und die Anthropic Messages API, und sie tut vier Dinge, die einzeln benannt werden sollten: Sie standardisiert das Client-seitige Format unabhängig von den Backends, wendet einen Satz Governance-Policies über Provider hinweg an, konfiguriert failover über Provider hinweg und entkoppelt Client-seitige Modellnamen von Backend-Modellnamen über Aliases.

Der Alias ist hier der wichtigste Teil, und er ist die direkte Fortsetzung von Teil 2. Ein Modell-Deployment, gepinnt auf gpt-4.1 Version 2025-04-14 mit NoAutoUpgrade, nützt nur etwas, wenn keine Anwendung es je beim Namen nennt. Anwendungen rufen chat-default auf. Der Alias mappt chat-default auf ein Deployment, und der Tag, an dem sich dieses Mapping ändert, ist eine Änderung an der Gateway-Konfiguration, reviewt in einem Pull Request, kein koordiniertes Release über fünf Teams hinweg.

Zwei Vorbehalte gehören hierher statt in ein Support-Ticket. Die unified model API ist in Preview und wird ausgerollt; in den klassischen Tiers läuft der frühe Zugang über den AI Gateway Early Release Channel, eine Update-Einstellung pro Instanz. Und ihre zwei unterstützten Backend-Formate decken nicht alles ab, was das Unternehmen betreibt. Mistral- und Llama-Deployments in Microsoft Foundry werden über ihre OpenAI-kompatible Chat-Completions-Oberfläche erreicht, die die unified API als Backend im OpenAI-Format vorschalten kann, aber das ist ein anderer Integrationspfad als die native Anthropic-Übersetzung, und es lohnt sich aufzuschreiben, welche Modelle über welchen Weg ankommen, bevor jemand um 2 Uhr nachts einen Shape-Mismatch debuggt.

Routing: erst Prioritätsgruppen, dann Gewichte

Hinter einem Alias sitzt ein backend pool, kein Backend. API-Management-Pools unterstützen Round-Robin-, gewichtete, prioritätsbasierte und session-aware Verteilung, und das nützliche Muster für Modell-Traffic kombiniert die ersten beiden: eine Prioritätsgruppe aus Provisioned-Throughput-Deployments, die alles aufnehmen soll, was sie kann, und eine niedriger priorisierte Gruppe aus Pay-as-you-go-Deployments, die nur Traffic sieht, wenn die erste Gruppe nicht verfügbar ist. Innerhalb einer Gruppe teilen Gewichte die Last auf.

Pools werden über das Portal, die REST API oder ein ARM- oder Bicep-Template konfiguriert. Teil 2 hat Terraform gewählt, und das ist genau der Fall, den jener Teil als legitim für die azapi-Notluke beschrieben hat: eine Ressource, ein roher ARM-Body, neben azurerm-Ressourcen und per ID auf sie verweisend.

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" },
        ]
      }
    }
  }
}

Priorität 1 ist das PTU-Deployment: Es ist bereits stundenweise bezahlt, jeder Token, den es bedienen kann, ist also ein Token, der nichts extra kostet. Priorität 2 bekommt nur Traffic, wenn Priorität 1 ihn nicht aufnehmen kann, und das 3-zu-1-Gewicht zwischen zwei Regionen spiegelt Quota wider, nicht Präferenz. Das ist eine Kostentopologie, ausgedrückt als Routing, und damit die Art von Ding, die in einem Architekturdiagramm unsichtbar ist und auf einer Rechnung offensichtlich.

Der circuit breaker, und wie weit man ihm trauen darf

Ein Pool-Mitglied, das fehlschlägt, sollte keinen Traffic mehr bekommen, ohne dass ein Mensch das entscheidet. API Management stellt einen circuit breaker als Eigenschaft des Backends bereit: Regeln definieren eine Fehleranzahl oder einen Fehlerprozentsatz innerhalb eines Intervalls sowie die Statuscode-Bereiche, die als Fehler zählen, und wenn der Breaker auslöst, ruft das Gateway dieses Backend für die Dauer des Trips nicht mehr auf und gibt dem Client 503 Service Unavailable zurück. Hat das Backend einen Retry-After-Header gesendet, kann der Breaker diesen Wert übernehmen und exakt so lange warten, was für einen rate-limitierten Modell-Endpoint deutlich besser ist als eine feste Schätzung.

circuitBreaker = {
  rules = [{
    name = "ptu-5xx"
    failureCondition = {
      count    = 3
      interval = "PT1H"
      statusCodeRanges = [{ min = 500, max = 599 }]
    }
    tripDuration     = "PT1H"
    acceptRetryAfter = true
  }]
}

Zwei Eigenschaften dieses Features bestimmen, wie man darüber argumentieren darf. Erstens: Die Auslöseregeln sind approximativ. Das Gateway ist verteilt, Instanzen synchronisieren den Breaker-Zustand nicht, und jede wendet die Regel auf die Informationen an, die sie hat. Ein Breaker, der nach drei Fehlern auslösen soll, lässt über eine skalierte Instanz hinweg in der Praxis womöglich deutlich mehr durch. Er ist ein Schutzmechanismus, kein Zähler, und keine SLO-Rechnung sollte auf seinem exakten Schwellwert aufbauen. Zweitens ist er im Consumption-Tier nicht verfügbar, ein Grund mehr, warum die Tier-Entscheidung in Teil 2 keine Kostenentscheidung war.

Die 503 lohnt es, bewusst zu designen. Ein Aufrufer, der vom Gateway eine 503 bekommt, kann nicht unterscheiden, ob das Modell down ist, der Breaker offen oder die ganze Instanz ungesund, und die drei Fälle verdienen unterschiedliches Client-Verhalten. Ein Response-Header in der Outbound-Policy, der benennt, welches Backend versucht wurde, plus geloggter Breaker-Zustand, macht aus einer opaken 503 etwas, worauf ein Consumer-Team reagieren kann, ohne ein Ticket zu öffnen.

Streaming, und was es die Buchhaltung kostet

Jedes Consumer-Team fragt nach streaming, und zu Recht. Time to first token ist die Zahl, die ein Mensch wahrnimmt; die Gesamtlatenz ist die Zahl, die ein Dashboard zeigt. Server-sent events über denselben HTTP-Endpoint sind die Standardantwort, der Client setzt stream: true, und am Routing des Gateways ändert sich nichts.

Was sich ändert, ist der Zähler. Die Token-Limit-Policy arbeitet normalerweise mit tatsächlicher usage: Mit estimate-prompt-tokens="false" liest sie den usage-Abschnitt der Modellantwort, ein Request kann das Limit also überschreiten und erst danach erkannt werden, was nachfolgende Requests blockiert, bis das Fenster zurückgesetzt wird. Mit aktivierter Schätzung zählt sie Prompt-Tokens vor dem Aufruf aus dem Schema der API-Definition und tauscht etwas Genauigkeit dagegen ein, keine Backend-Quota zu verbrennen, nur um festzustellen, dass man drüber war.

Streaming nimmt einem diese Wahl. Ist stream: true gesetzt, werden Prompt-Tokens immer geschätzt, unabhängig von der Policy-Einstellung, und Completion-Tokens ebenfalls. Es gibt keine Konfiguration, die einen gestreamten Request am Gateway aus der Ground Truth misst. Für Modelle, die Bilder akzeptieren, verstärkt sich die Abweichung: Mit aktiviertem streaming, oder mit aktivierter Schätzung, überzählt die Policy Bild-Tokens, statt den Wert des Backends zu übernehmen.

Das ist kein Grund, streaming abzulehnen. Es ist ein Grund, präzise zu benennen, was die Zahlen bedeuten, denn Teil 5 macht daraus Geld:

  • Nicht gestreamtes HTTP: gemessen aus der usage des Modells selbst. Exakt.
  • Gestreamtes SSE: Prompt- und Completion-Tokens am Gateway geschätzt. Gut genug für Rate Limiting, approximativ für Abrechnung.
  • WebSocket: siehe unten. Faktisch out of band.

Die Antwort der Control Plane ist Abgleich statt besserer Schätzung. Gateway-Metriken bleiben das Echtzeitsignal, das Quotas durchsetzt, und der monatliche Attributionsjob, den Teil 5 baut, korrigiert gegen die autoritative usage, die der Provider meldet, mit der Differenz als eigener Metrik. Eine Drift, die wächst, ist ein Bug; eine Drift, die flach bei wenigen Prozent bleibt, ist der Preis des streaming, und sie gehört ins Chargeback-Dokument geschrieben, statt von einem Product Owner entdeckt zu werden.

WebSockets, wo die Abstraktion endet

Echtzeit-Voice- und Speech-to-Speech-Traffic passt nicht in Request-Response, und API Management unterstützt WebSocket-APIs. Die Einschränkungen sind scharf genug, dass sie die Architektur verändern, statt sie zu dekorieren.

WebSocket-Verbindungen können nicht über mehrere Backends verteilt oder per load balancing gestreut werden. Einmal aufgebaut, wird jede Verbindung eins-zu-eins zwischen Client und Backend gehalten. Jede Routing-Entscheidung in diesem Post, die Prioritätsgruppen, die PTU-first-Topologie, die gewichtete regionale Aufteilung, gilt für den initialen Handshake und für nichts danach. Ein Backend, das mitten in der Session degradiert, kann vom Gateway nicht per failover ersetzt werden; die Session muss enden, und der Client muss neu verbinden. Jeder Client auf diesem Pfad braucht Reconnect-Logik mit Backoff, und jedes Kapazitätsmodell muss eine WebSocket-Session als gepinnte Reservierung behandeln statt als Strom unabhängig routbarer Requests.

Drei kleinere Einschränkungen folgen daraus. Eine lange Liste von Policies kann nicht auf die onHandshake-Operation angewendet werden, darunter Caching, CORS, Body-Manipulation und jede validate-*-Policy, und Policies, die aus einem globalen oder Produkt-Scope geerbt werden, werden zur Laufzeit übersprungen, statt laut zu scheitern. Die set-header-Policy kann bestimmte Well-known-Header, darunter Host, auf Handshake-Requests nicht ändern. Und die TLS-Validierung ist strenger als bei HTTP-APIs: Für ein WebSocket-Backend validiert das Gateway, dass dem Serverzertifikat vertraut wird und dass sein Subject Name zum Hostnamen passt, wo eine HTTP-API nur Vertrauen verlangt. Ein Backend mit unpassendem Zertifikats-Subject, das über HTTP jahrelang funktioniert hat, scheitert in dem Moment, in dem es als WebSocket-API vorgeschaltet wird.

Die praktische Konsequenz: Der Echtzeitpfad bekommt eine eigene API, ein eigenes Produkt und eine eigene Quota, und die Consumer-Dokumentation sagt klar, dass er nicht von den failover-Garantien des Chat-Endpoints abgedeckt ist. So zu tun, als wäre er es, ist der Weg, auf dem ein Incident Review den Unterschied entdeckt.

Session affinity, und warum man sie besser nicht braucht

Pools unterstützen Session-Awareness über ein Cookie: Der Client speichert einen Set-Cookie-Wert und gibt ihn zurück, sodass nachfolgende Requests dasselbe Backend erreichen. Das existiert für zustandsbehaftete serverseitige APIs, die Assistants API als kanonisches Beispiel, wo der Client eine Session-ID halten, eine Thread-ID aus dem Response-Body extrahieren und das richtige Cookie beim richtigen Aufruf senden muss.

Es funktioniert, und es ist eine Einschränkung, die man zur Designzeit besser vermeidet. Ein session-affiner Aufruf ist ein Aufruf, der nicht umgeroutet werden kann, was ihn in dieselbe Kategorie stellt wie den WebSocket-Pfad: kein failover, und ein Kapazitätsmodell, in dem die Last eines Backends davon abhängt, welche Sessions darauf gelandet sind, statt davon, wie viele Requests ankamen. Wo eine zustandsbehaftete API wirklich nötig ist, gehört sie auf einen eigenen Alias, damit ihre schwächeren Garantien nicht stillschweigend vom zustandslosen Traffic geerbt werden, der den Rest des Volumens ausmacht.

Fehlerbilder, die man im Blick behalten sollte

  • Die 503, die vier verschiedene Dinge bedeutet. Breaker offen, alle Pool-Mitglieder erschöpft, Gateway ungesund oder ein tatsächlich fehlschlagendes Backend. Ohne Header oder Log-Zeile, die benennt, welcher Fall vorliegt, wird jeder einzelne zum Ticket.
  • Streaming-Drift als Bug behandelt. Sie ist dokumentiertes Verhalten. Der Fehler ist nicht die Drift, sondern eine geschätzte Zahl an einer Stelle zu veröffentlichen, auf der Rechnung steht.
  • Ein Preview-Feature im kritischen Pfad. Die unified model API ist in Preview. Das ist für die Übersetzungsschicht nur akzeptabel, wenn der Fallback, eine direkte Per-Provider-API hinter demselben Produkt und denselben Policies, bereits existiert und getestet ist, statt während des Incidents entworfen zu werden.
  • Still übersprungene Policies auf WebSocket-APIs. Geerbte Policies, die auf onHandshake nicht unterstützt werden, werden zur Laufzeit übersprungen. Eine Sicherheitskontrolle, die als global angenommen wird, ist hier nicht global, und nichts wird es einem sagen.

Was Teil 4 erbt

Am Ende dieses Teils hat ein Consumer-Team einen Endpoint, ein Format und stabile Modell-Aliases, mit Routing und failover dahinter und einer dokumentierten Aussage, welche Garantien für welchen Transport gelten. Was nichts davon abdeckt, ist Arbeit, die keine Antwort sofort braucht: die nächtliche Zusammenfassung von Support-Tickets, die sonst auf demselben synchronen Pfad säße wie ein Kunde, der auf den ersten Token wartet. Das ist eine Queue, und sie kommt als Nächstes.

Weiterlesen

Für die Infrastruktur- und Plattform-Seite, so etwas in diesem Maßstab zu betreiben, gibt es die Field Notes auf ercan.cloud, und der Hub liegt auf ercanermis.com.

Referenzen