Οι εγγυήσεις του gateway δεν είναι ιδιότητες του gateway. Είναι ιδιότητες του transport που διάλεξε ο καλών, και εξασθενούν με κάθε βήμα προς το real-time. Μια απλή κλήση request-response μετριέται από το usage block που πραγματικά επέστρεψε το μοντέλο. Η ίδια κλήση με stream: true έχει τα prompt tokens της εκτιμημένα ό,τι κι αν λέει η πολιτική, και τα completion tokens της εκτιμημένα επίσης. Μια σύνδεση WebSocket δεν μπορεί να γίνει load balanced ανάμεσα σε backends καθόλου, επειδή μόλις εγκαθιδρυθεί μένει καρφωμένη ένα-προς-ένα ανάμεσα σε client και backend. Το Μέρος 2 έχτισε την υποδομή. Αυτό το μέρος βάζει ένα API πάνω της, και η τίμια εκδοχή αυτού του API είναι μια που λέει σε κάθε καλούντα ποιες εγγυήσεις μόλις παράτησε.

Τι θα έπρεπε να χρειάζεται να ξέρει μια ομάδα-καταναλωτής

Ακριβώς τρία πράγματα: ένα base URL, ένα client credential, και ένα όνομα μοντέλου που σημαίνει κάτι για εκείνη και όχι για το Azure. Τίποτα για deployments, regions, providers, ή ποιο από αυτά άλλαξε την περασμένη Τρίτη. Αυτό είναι ολόκληρο το συμβόλαιο, και ό,τι υπάρχει σε αυτό το μέρος υπάρχει για να το κρατά αληθινό ενώ τα πράγματα πίσω του κινούνται.

Το unified model API του API Management είναι φτιαγμένο ακριβώς για αυτό. Οι clients μιλούν το φορμά OpenAI Chat Completions, και το gateway μεταφράζει σε όποιο backend φορμά χρησιμοποιεί πραγματικά το μοντέλο-στόχος. Υποστηρίζει δύο backend API φορμά, το OpenAI Chat Completions και το Anthropic Messages API, και κάνει τέσσερα πράγματα που αξίζει να ονομαστούν χωριστά: τυποποιεί το φορμά προς τον client ανεξάρτητα από τα backends, εφαρμόζει ένα ενιαίο σύνολο πολιτικών διακυβέρνησης σε όλους τους providers, ρυθμίζει failover ανάμεσα σε providers, και αποσυνδέει τα ονόματα μοντέλων προς τον client από τα ονόματα μοντέλων του backend με aliases.

Το alias είναι το κομμάτι που μετράει περισσότερο εδώ, και είναι η άμεση συνέχεια του Μέρους 2. Ένα model deployment καρφωμένο στο gpt-4.1 έκδοση 2025-04-14 με NoAutoUpgrade είναι χρήσιμο μόνο αν καμία εφαρμογή δεν το ονομάζει ποτέ. Οι εφαρμογές καλούν το chat-default. Το alias αντιστοιχίζει το chat-default σε ένα deployment, και η μέρα που αυτή η αντιστοίχιση αλλάζει είναι μια αλλαγή στη διαμόρφωση του gateway που επιθεωρείται σε ένα pull request, όχι ένα συντονισμένο release σε πέντε ομάδες.

Δύο επιφυλάξεις ανήκουν εδώ και όχι σε ένα support ticket. Το unified model API είναι σε preview και βρίσκεται σε φάση σταδιακής διάθεσης· στα κλασικά tiers η πρώιμη πρόσβαση περνά από το AI Gateway Early release channel, που είναι μια ρύθμιση ενημέρωσης ανά instance. Και τα δύο υποστηριζόμενα backend φορμά του δεν καλύπτουν όλα όσα τρέχει η εταιρεία. Τα deployments Mistral και Llama στο Microsoft Foundry προσεγγίζονται μέσω της OpenAI-συμβατής επιφάνειας chat completions που εκθέτουν, την οποία το unified API μπορεί να παρουσιάσει ως backend σε φορμά OpenAI, αλλά αυτό είναι διαφορετικό μονοπάτι ενσωμάτωσης από την εγγενή μετάφραση Anthropic, και αξίζει να καταγραφεί ποια μοντέλα φτάνουν από ποια διαδρομή πριν κάνει κάποιος debugging σε ένα shape mismatch στις 2 τα ξημερώματα.

Δρομολόγηση: πρώτα priority groups, μετά βάρη

Πίσω από ένα alias κάθεται ένα backend pool, όχι ένα backend. Τα pools του API Management υποστηρίζουν round-robin, weighted, priority-based και session-aware κατανομή, και το χρήσιμο μοτίβο για traffic μοντέλων συνδυάζει τα δύο πρώτα: ένα priority group από Provisioned Throughput deployments που πρέπει να απορροφούν ό,τι μπορούν, και ένα group χαμηλότερης προτεραιότητας από pay-as-you-go deployments που βλέπει traffic μόνο όταν το πρώτο group δεν είναι διαθέσιμο. Μέσα σε ένα group, τα βάρη μοιράζουν το φορτίο.

Τα pools ρυθμίζονται μέσω του portal, του REST API, ή ενός ARM ή Bicep template. Το Μέρος 2 διάλεξε Terraform, και αυτή είναι ακριβώς η περίπτωση που εκείνο το μέρος περιέγραψε ως νόμιμη για την πόρτα διαφυγής azapi: ένας πόρος, ένα ακατέργαστο ARM body, που κάθεται δίπλα σε πόρους azurerm και τους αναφέρει μέσω 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" },
        ]
      }
    }
  }
}

Το priority 1 είναι το PTU deployment: είναι ήδη πληρωμένο με την ώρα, οπότε κάθε token που μπορεί να εξυπηρετήσει είναι ένα token που δεν κοστίζει τίποτα επιπλέον. Το priority 2 δέχεται traffic μόνο όταν το priority 1 δεν μπορεί να το πάρει, και το βάρος 3-προς-1 ανάμεσα σε δύο regions αντανακλά quota και όχι προτίμηση. Αυτή είναι μια τοπολογία κόστους εκφρασμένη ως δρομολόγηση, το είδος του πράγματος που είναι αόρατο σε ένα διάγραμμα αρχιτεκτονικής και προφανές σε έναν λογαριασμό.

Το circuit breaker, και πόσο να το εμπιστεύεσαι

Ένα μέλος του pool που αποτυγχάνει πρέπει να σταματά να δέχεται traffic χωρίς να χρειάζεται να το αποφασίσει άνθρωπος. Το API Management εκθέτει ένα circuit breaker ως ιδιότητα του backend: οι κανόνες ορίζουν έναν αριθμό ή ένα ποσοστό αποτυχιών μέσα σε ένα διάστημα και τα εύρη status codes που μετρούν ως αποτυχία, και όταν το breaker ανοίξει, το gateway σταματά να καλεί αυτό το backend για τη διάρκεια του trip και επιστρέφει 503 Service Unavailable στον client. Αν το backend έστειλε ένα header Retry-After, το breaker μπορεί να δεχτεί αυτή την τιμή και να περιμένει ακριβώς τόσο, κάτι που για ένα rate-limited endpoint μοντέλου είναι πολύ καλύτερο από μια σταθερή εικασία.

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

Δύο ιδιότητες αυτής της δυνατότητας καθορίζουν πώς επιτρέπεται να σκέφτεσαι γι' αυτήν. Πρώτον, οι κανόνες trip είναι προσεγγιστικοί: το gateway είναι κατανεμημένο, τα instances δεν συγχρονίζουν την κατάσταση του breaker, και το καθένα εφαρμόζει τον κανόνα με τις πληροφορίες που έχει. Ένα breaker ρυθμισμένο να ανοίγει μετά από τρεις αποτυχίες μπορεί στην πράξη να αφήσει να περάσουν αρκετά περισσότερες σε ένα scaled-out instance. Είναι μηχανισμός προστασίας, όχι μετρητής, και κανένας υπολογισμός SLO δεν πρέπει να χτίζεται πάνω στο ακριβές κατώφλι του. Δεύτερον, δεν είναι διαθέσιμο στο Consumption tier, ένας ακόμη λόγος που η απόφαση για το tier στο Μέρος 2 δεν ήταν απόφαση κόστους.

Το 503 αξίζει να σχεδιαστεί σκόπιμα. Ένας καλών που δέχεται 503 από το gateway δεν μπορεί να ξέρει αν το μοντέλο είναι εκτός λειτουργίας, αν το breaker είναι ανοιχτό, ή αν ολόκληρο το instance είναι ασθενές, και τα τρία αξίζουν διαφορετική συμπεριφορά από τον client. Το να προστεθεί ένα response header στην outbound πολιτική που ονομάζει ποιο backend δοκιμάστηκε, και να καταγράφεται η κατάσταση του breaker, μετατρέπει ένα αδιαφανές 503 σε κάτι πάνω στο οποίο μια ομάδα-καταναλωτής μπορεί να δράσει χωρίς να ανοίξει ticket.

Streaming, και η λογιστική που σου κοστίζει

Κάθε ομάδα-καταναλωτής ζητά streaming, και δικαίως. Ο χρόνος μέχρι το πρώτο token είναι το νούμερο που αντιλαμβάνεται ένας άνθρωπος· το συνολικό latency είναι το νούμερο που δείχνει ένα dashboard. Τα server-sent events πάνω από το ίδιο HTTP endpoint είναι η καθιερωμένη απάντηση, ο client ορίζει stream: true, και τίποτα στη δρομολόγηση του gateway δεν αλλάζει.

Αυτό που αλλάζει είναι ο μετρητής. Η πολιτική ορίου tokens δουλεύει κανονικά από πραγματική χρήση: με estimate-prompt-tokens="false" διαβάζει την ενότητα usage της απάντησης του μοντέλου, που σημαίνει ότι ένα αίτημα μπορεί να ξεπεράσει το όριο και να εντοπιστεί εκ των υστέρων, μπλοκάροντας τα επόμενα αιτήματα μέχρι να μηδενίσει το παράθυρο. Με την εκτίμηση ενεργή, μετρά τα prompt tokens από το σχήμα του ορισμού του API πριν από την κλήση, ανταλλάσσοντας λίγη ακρίβεια με το να μην καίει quota του backend για να ανακαλύψεις ότι είχες ήδη ξεπεράσει το όριο.

Το streaming αφαιρεί την επιλογή. Όταν οριστεί stream: true, τα prompt tokens εκτιμώνται πάντα ανεξάρτητα από τη ρύθμιση της πολιτικής, και τα completion tokens εκτιμώνται επίσης. Δεν υπάρχει διαμόρφωση που να κάνει ένα streamed αίτημα να μετριέται από την πραγματική τιμή στο gateway. Για μοντέλα που δέχονται εικόνες η απόκλιση συσσωρεύεται: με το streaming ενεργό, ή με την εκτίμηση ενεργή, η πολιτική υπερμετρά τα image tokens αντί να παίρνει τη μέτρηση του backend.

Αυτό δεν είναι λόγος να αρνηθείς το streaming. Είναι λόγος να είσαι ακριβής για το τι σημαίνουν τα νούμερα, επειδή το Μέρος 5 τα μετατρέπει σε χρήματα:

  • Μη streamed HTTP: μετριέται από το ίδιο το usage του μοντέλου. Ακριβές.
  • Streamed SSE: prompt και completion tokens εκτιμώνται στο gateway. Αρκετά καλό για rate limiting, προσεγγιστικό για χρέωση.
  • WebSocket: δες παρακάτω. Ουσιαστικά εκτός μέτρησης.

Η απάντηση του control plane είναι η συμφωνία των στοιχείων, όχι η καλύτερη εκτίμηση. Τα metrics του gateway παραμένουν το real-time σήμα που επιβάλλει τα quotas, και η μηνιαία εργασία απόδοσης κόστους που χτίζει το Μέρος 5 διορθώνει με βάση όποια αυθεντική χρήση αναφέρει ο provider, με το delta να παρακολουθείται ως δικό του metric. Μια απόκλιση που μεγαλώνει είναι bug· μια απόκλιση που μένει σταθερή σε λίγα τοις εκατό είναι το κόστος του streaming, και πρέπει να γραφτεί στο έγγραφο του chargeback αντί να ανακαλυφθεί από έναν product owner.

WebSockets, εκεί που τελειώνει το abstraction

Το real-time traffic φωνής και speech-to-speech δεν χωράει στο request-response, και το API Management υποστηρίζει WebSocket APIs. Οι περιορισμοί είναι αρκετά αιχμηροί ώστε να αλλάζουν την αρχιτεκτονική αντί απλώς να τη διακοσμούν.

Οι συνδέσεις WebSocket δεν μπορούν να κατανεμηθούν ή να γίνουν load balanced ανάμεσα σε πολλαπλά backends. Μόλις εγκαθιδρυθεί, κάθε σύνδεση διατηρείται ένα-προς-ένα ανάμεσα σε client και backend. Κάθε απόφαση δρομολόγησης σε αυτό το post, τα priority groups, η τοπολογία PTU-first, ο σταθμισμένος διαχωρισμός ανά region, ισχύει για το αρχικό handshake και για τίποτα μετά από αυτό. Ένα backend που υποβαθμίζεται στη μέση ενός session δεν μπορεί να γίνει failover από το gateway· το session πρέπει να τελειώσει και ο client πρέπει να επανασυνδεθεί. Κάθε client σε αυτό το μονοπάτι χρειάζεται λογική επανασύνδεσης με backoff, και κάθε μοντέλο χωρητικότητας πρέπει να αντιμετωπίζει ένα WebSocket session ως καρφωμένη κράτηση και όχι ως ροή ανεξάρτητα δρομολογήσιμων αιτημάτων.

Τρεις μικρότεροι περιορισμοί ακολουθούν. Μια μακριά λίστα πολιτικών δεν μπορεί να εφαρμοστεί στη λειτουργία onHandshake, συμπεριλαμβανομένων caching, CORS, χειρισμού body, και κάθε πολιτικής validate-*, και οι πολιτικές που κληρονομούνται από global ή product scope παραλείπονται στο runtime αντί να αποτύχουν φωναχτά. Η πολιτική set-header δεν μπορεί να αλλάξει ορισμένα well-known headers, συμπεριλαμβανομένου του Host, σε handshake αιτήματα. Και η επικύρωση TLS είναι αυστηρότερη από ό,τι για HTTP APIs: για ένα WebSocket backend το gateway επικυρώνει ότι το πιστοποιητικό του server είναι έμπιστο και ότι το subject name του ταιριάζει με το hostname, ενώ ένα HTTP API απαιτεί μόνο εμπιστοσύνη. Ένα backend με αναντίστοιχο subject στο πιστοποιητικό που δούλευε χρόνια πάνω από HTTP θα αποτύχει τη στιγμή που θα μπει μπροστά του ως WebSocket API.

Η πρακτική συνέπεια: το real-time μονοπάτι παίρνει δικό του API, δικό του product, και δικό του quota, και η τεκμηρίωση προς τους καταναλωτές λέει καθαρά ότι δεν καλύπτεται από τις εγγυήσεις failover που έχει το chat endpoint. Το να προσποιείσαι το αντίθετο είναι ο τρόπος με τον οποίο ένα incident review ανακαλύπτει τη διαφορά.

Session affinity, και γιατί να αποφύγεις να το χρειάζεσαι

Τα pools υποστηρίζουν session awareness μέσω cookie, με τον client να αποθηκεύει μια τιμή Set-Cookie και να την επιστρέφει ώστε τα επόμενα αιτήματα να φτάνουν στο ίδιο backend. Υπάρχει για stateful server-side APIs, με το Assistants API ως το κανονικό παράδειγμα, όπου ο client πρέπει να κρατά ένα session ID, να εξάγει ένα thread ID από το response body, και να στέλνει το σωστό cookie στη σωστή κλήση.

Δουλεύει, και είναι ένας περιορισμός που αξίζει να αποφευχθεί ήδη στον σχεδιασμό. Μια κλήση με session affinity είναι μια κλήση που δεν μπορεί να αναδρομολογηθεί, κάτι που τη βάζει στην ίδια κατηγορία με το μονοπάτι WebSocket: κανένα failover, και ένα μοντέλο χωρητικότητας όπου το φορτίο ενός backend εξαρτάται από το ποια sessions προσγειώθηκαν σε αυτό και όχι από το πόσα αιτήματα έφτασαν. Εκεί που ένα stateful API πραγματικά απαιτείται, κράτησέ το σε χωριστό alias, ώστε οι πιο αδύναμες εγγυήσεις του να μην κληρονομούνται σιωπηλά από το stateless traffic που αποτελεί τον υπόλοιπο όγκο.

Τρόποι αποτυχίας που αξίζει να προσέχεις

  • Το 503 που σημαίνει τέσσερα διαφορετικά πράγματα. Breaker ανοιχτό, όλα τα μέλη του pool εξαντλημένα, gateway ασθενές, ή ένα backend που πραγματικά αποτυγχάνει. Χωρίς ένα header ή μια γραμμή στο log που ονομάζει ποιο από αυτά ισχύει, καθένα τους γίνεται ticket.
  • Η απόκλιση του streaming να αντιμετωπίζεται ως bug. Είναι τεκμηριωμένη συμπεριφορά. Η αποτυχία δεν είναι η απόκλιση, είναι η δημοσίευση ενός εκτιμημένου νούμερου σε ένα μέρος που γράφει τιμολόγιο.
  • Μια δυνατότητα σε preview στο κρίσιμο μονοπάτι. Το unified model API είναι σε preview. Αυτό είναι αποδεκτό για το στρώμα μετάφρασης μόνο αν το fallback, ένα απευθείας API ανά provider πίσω από το ίδιο product και τις ίδιες πολιτικές, υπάρχει ήδη και έχει δοκιμαστεί, αντί να σχεδιάζεται στη διάρκεια του περιστατικού.
  • Σιωπηλά παραλειπόμενες πολιτικές στα WebSocket APIs. Οι κληρονομημένες πολιτικές που δεν υποστηρίζονται στο onHandshake παραλείπονται στο runtime. Ένας έλεγχος ασφάλειας που θεωρείται καθολικός δεν είναι καθολικός εδώ, και τίποτα δεν θα σε ειδοποιήσει.

Τι κληρονομεί το Μέρος 4

Στο τέλος αυτού του μέρους μια ομάδα-καταναλωτής έχει ένα endpoint, ένα φορμά, και σταθερά model aliases, με δρομολόγηση και failover πίσω τους και μια τεκμηριωμένη δήλωση για το ποιες εγγυήσεις ισχύουν σε ποιο transport. Αυτό που τίποτα από όλα αυτά δεν χειρίζεται είναι η δουλειά που δεν χρειάζεται απάντηση τώρα: η νυχτερινή σύνοψη των tickets υποστήριξης που αλλιώς θα καθόταν στο ίδιο σύγχρονο μονοπάτι με έναν πελάτη που περιμένει το πρώτο token. Αυτό είναι μια ουρά, και είναι το επόμενο.

Διάβασε επίσης

Για την πλευρά υποδομής και πλατφόρμας της λειτουργίας κάτι τέτοιου σε κλίμακα, οι σημειώσεις πεδίου είναι στο ercan.cloud, και ο κόμβος είναι στο ercanermis.com.

Αναφορές