feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)
Initial addition of ms-ai-architect plugin to the open-source marketplace. Private content excluded: orchestrator/ (Linear tooling), docs/utredning/ (client investigation), generated test reports and PDF export script. skill-gen tooling moved from orchestrator/ to scripts/skill-gen/. Security scan: WARNING (risk 20/100) — no secrets, no injection found. False positive fixed: added gitleaks:allow to Python variable reference in output-validation-grounding-verification.md line 109. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a8d79e4484
commit
6a7632146e
490 changed files with 213249 additions and 2 deletions
|
|
@ -0,0 +1,397 @@
|
|||
# APIM as AI Gateway: Architecture & Concepts
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Azure API Management (APIM) har utviklet seg fra en tradisjonell API-gateway til en fullverdig AI-gateway som gir organisasjoner sentral kontroll over alle generative AI-tjenester. For norsk offentlig sektor, der mange etater deler Azure OpenAI-instanser på tvers av avdelinger og prosjekter, er APIM den anbefalte tilnærmingen for å sikre styring, kostnadsfordeling og sikkerhet.
|
||||
|
||||
APIM som AI-gateway kombinerer tradisjonelle API Management-funksjoner (autentisering, rate limiting, logging) med spesialiserte AI-kapabiliteter som token-basert kvoteregulering, semantisk caching, multi-modell backend routing og innholdssikkerhet. Microsoft anbefaler APIM som den foretrukne PaaS-løsningen for å bygge og drifte en AI-gateway, fremfor egenutviklede løsninger.
|
||||
|
||||
I en typisk enterprise-arkitektur sitter APIM mellom klientapplikasjoner (chatbots, agentrammeverk, RAG-pipelines) og backend AI-tjenester (Azure OpenAI, Azure AI Foundry, tredjepartsmodeller). Dette gir ett enkelt endepunkt for alle konsumenter, uavhengig av hvor mange backend-instanser som finnes bak gatewayen.
|
||||
|
||||
---
|
||||
|
||||
## Kjernekonsepter i Azure API Management
|
||||
|
||||
### APIM-arkitektur
|
||||
|
||||
Azure API Management består av tre hovedkomponenter:
|
||||
|
||||
| Komponent | Rolle | Beskrivelse |
|
||||
|-----------|-------|-------------|
|
||||
| **API Gateway** | Data plane / runtime | Mottar API-kall, håndhever policies, videresender til backends |
|
||||
| **Management Plane** | Kontrollplan | Konfigurering av APIs, policies, backends, produkter |
|
||||
| **Developer Portal** | Selvbetjening | API-dokumentasjon, testing, onboarding av utviklere |
|
||||
|
||||
### APIM Service Tiers for AI
|
||||
|
||||
| Tier | AI Gateway-støtte | Circuit Breaker | Semantic Caching | Token Policies | Anbefaling |
|
||||
|------|-------------------|-----------------|-------------------|----------------|------------|
|
||||
| **Consumption** | Begrenset | Nei | Ja | Nei (ingen token limit by key) | Ikke anbefalt for AI |
|
||||
| **Developer** | Full | Ja | Ja | Ja | Dev/test |
|
||||
| **Basic v2** | Full | Ja | Ja | Ja | Små workloads |
|
||||
| **Standard v2** | Full | Ja | Ja | Ja | Produksjon |
|
||||
| **Premium** | Full + multi-region | Ja | Ja | Ja | Enterprise / offentlig sektor |
|
||||
|
||||
**Anbefaling for norsk offentlig sektor:** Standard v2 eller Premium, avhengig av krav til multi-region og VNet-integrasjon.
|
||||
|
||||
---
|
||||
|
||||
## AI Gateway-kapabiliteter
|
||||
|
||||
APIM tilbyr fem hovedkategorier av AI-spesifikke funksjoner:
|
||||
|
||||
### 1. Token Rate Limiting og Kvoter
|
||||
|
||||
Kontroller token-forbruk per konsument med dedikerte policies:
|
||||
|
||||
```xml
|
||||
<!-- Begrens tokens per minutt per subscription -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="10000"
|
||||
estimate-prompt-tokens="true"
|
||||
remaining-tokens-variable-name="remainingTokens">
|
||||
</llm-token-limit>
|
||||
```
|
||||
|
||||
Policies for Azure OpenAI-spesifikke og generelle LLM-scenarier:
|
||||
|
||||
| Policy | Scope | Beskrivelse |
|
||||
|--------|-------|-------------|
|
||||
| `azure-openai-token-limit` | Azure OpenAI | Token-begrensning spesifikt for Azure OpenAI-endepunkter |
|
||||
| `llm-token-limit` | Alle LLM-er | Generell token-begrensning for alle LLM APIs |
|
||||
| `azure-openai-emit-token-metric` | Azure OpenAI | Emit token-metrikk til Application Insights |
|
||||
| `llm-emit-token-metric` | Alle LLM-er | Generell token-metrikk for alle LLM APIs |
|
||||
|
||||
### 2. Load Balancing
|
||||
|
||||
Backend pools med round-robin, weighted og priority-basert lastbalansering:
|
||||
|
||||
```xml
|
||||
<!-- Rut trafikk til backend pool -->
|
||||
<set-backend-service backend-id="openai-pool" />
|
||||
```
|
||||
|
||||
Load balancing-alternativer:
|
||||
|
||||
| Modus | Beskrivelse | Typisk bruk |
|
||||
|-------|-------------|-------------|
|
||||
| **Round-robin** | Jevn fordeling mellom backends | Standard, like instanser |
|
||||
| **Weighted** | Vektet fordeling basert på kapasitet | Blue-green deployments |
|
||||
| **Priority-based** | Prioritetsgrupper, fallback ved feil | PTU + Pay-as-you-go spillover |
|
||||
| **Session-aware** | Sesjonsaffinitet via cookie | Chat-assistenter, Assistants API |
|
||||
|
||||
### 3. Circuit Breaker
|
||||
|
||||
Beskytter backend-tjenester mot overbelastning med automatisk feilhåndtering:
|
||||
|
||||
| Egenskap | Beskrivelse |
|
||||
|----------|-------------|
|
||||
| **Failure threshold** | Antall feil som utløser circuit breaker |
|
||||
| **Trip duration** | Varighet circuit breaker er åpen |
|
||||
| **Retry-After header** | Dynamisk trip duration basert på backend-respons |
|
||||
| **Status code range** | Hvilke HTTP-koder som teller som feil (f.eks. 429, 5xx) |
|
||||
|
||||
### 4. Semantic Caching
|
||||
|
||||
Gjenbruk av LLM-svar basert på semantisk likhet:
|
||||
|
||||
```xml
|
||||
<!-- Inbound: Sjekk cache -->
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
ignore-system-messages="true"
|
||||
max-message-count="10">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
|
||||
<!-- Outbound: Lagre i cache -->
|
||||
<azure-openai-semantic-cache-store duration="3600" />
|
||||
```
|
||||
|
||||
**Krav:** Azure Managed Redis med RediSearch-modul.
|
||||
|
||||
### 5. Sikkerhet og Content Safety
|
||||
|
||||
| Funksjon | Policy/Mekanisme | Beskrivelse |
|
||||
|----------|-------------------|-------------|
|
||||
| **Autentisering** | Managed Identity | Eliminerer API-nøkler, bruker system- eller user-assigned identity |
|
||||
| **Content Safety** | `llm-content-safety` | Automatisk moderering via Azure AI Content Safety |
|
||||
| **OAuth** | Credential Manager | OAuth-autorisasjon for AI-apper og agenter |
|
||||
| **MCP-sikkerhet** | Secure MCP servers | Sikrer tilgang til MCP-servere via APIM |
|
||||
|
||||
---
|
||||
|
||||
## Arkitekturmønstre for AI Gateway
|
||||
|
||||
### Mønster 1: Sentralisert AI Gateway (anbefalt)
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Azure API Mgmt │
|
||||
│ (AI Gateway) │
|
||||
Chatbot ─────────────►│ │──► Azure OpenAI (Norway East)
|
||||
RAG Pipeline ────────►│ - Token limiting │──► Azure OpenAI (Sweden Central)
|
||||
Copilot Studio ─────►│ - Load balancing │──► Azure AI Foundry
|
||||
Power Automate ─────►│ - Circuit breaker │──► Third-party LLMs
|
||||
│ - Caching │
|
||||
│ - Content safety │
|
||||
└─────────────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ Monitoring │
|
||||
│ App Insights│
|
||||
│ Log Analyt. │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
**Fordeler:**
|
||||
- Ett endepunkt for alle konsumenter
|
||||
- Sentralisert styring og kostnadskontroll
|
||||
- Konsistent sikkerhetspolitikk
|
||||
- Full observabilitet av token-forbruk
|
||||
|
||||
### Mønster 2: Multi-Region AI Gateway
|
||||
|
||||
```
|
||||
Client ──► DNS/Traffic Manager
|
||||
│
|
||||
┌────────┴────────┐
|
||||
▼ ▼
|
||||
APIM Gateway APIM Gateway
|
||||
(Norway East) (Sweden Central)
|
||||
│ │
|
||||
▼ ▼
|
||||
Azure OpenAI Azure OpenAI
|
||||
(Norway East) (Sweden Central)
|
||||
```
|
||||
|
||||
For norsk offentlig sektor med krav til datasuverenitet:
|
||||
- Deploy APIM Premium med multi-region
|
||||
- Regionalt avgrensede backends via policy-logikk
|
||||
- Innebygd FQDN-routing basert på laveste latens
|
||||
|
||||
### Mønster 3: Hub-and-Spoke for offentlig sektor
|
||||
|
||||
```
|
||||
Central IT (Hub)
|
||||
┌──────────────────────────┐
|
||||
│ APIM (Premium) │
|
||||
│ - Sentral policy │
|
||||
│ - Kostnadsallokering │
|
||||
│ - Compliance monitoring │
|
||||
└──────┬───────┬───────────┘
|
||||
│ │
|
||||
┌──────────┘ └──────────┐
|
||||
▼ ▼
|
||||
Etat A (Spoke) Etat B (Spoke)
|
||||
- Subscription A - Subscription B
|
||||
- TPM: 50 000 - TPM: 30 000
|
||||
- Egne produkter - Egne produkter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Governance og organisatorisk styring
|
||||
|
||||
### Kostnadsfordeling med APIM
|
||||
|
||||
APIM muliggjør presis kostnadsfordeling gjennom:
|
||||
|
||||
| Mekanisme | Hvordan | Eksempel |
|
||||
|-----------|---------|---------|
|
||||
| **Subscription keys** | Hvert team/prosjekt får egen subscription | Team A: 50k TPM, Team B: 30k TPM |
|
||||
| **Products** | Grupperer APIer med ulike kvoter | "Standard AI" (10k TPM), "Premium AI" (100k TPM) |
|
||||
| **Custom headers** | Spor forbruk per bruker/applikasjon | `x-cost-center: 12345` |
|
||||
| **Token metrics** | Emit til Application Insights per dimensjon | Dashboard per team, API, bruker |
|
||||
|
||||
### Eksempel: Token-metrikk med dimensjoner
|
||||
|
||||
```xml
|
||||
<llm-emit-token-metric namespace="ai-gateway-metrics">
|
||||
<dimension name="Etat" value="@(context.Request.Headers.GetValueOrDefault("x-etat", "ukjent"))" />
|
||||
<dimension name="Prosjekt" value="@(context.Request.Headers.GetValueOrDefault("x-prosjekt", "ukjent"))" />
|
||||
<dimension name="API" value="@(context.Api.Id)" />
|
||||
<dimension name="Modell" value="@(context.Request.Headers.GetValueOrDefault("x-model-id", "default"))" />
|
||||
</llm-emit-token-metric>
|
||||
```
|
||||
|
||||
### Observabilitet
|
||||
|
||||
APIM integreres med Azure Monitor for full oversikt:
|
||||
|
||||
| Datakilde | Hva den gir | Verktøy |
|
||||
|-----------|-------------|---------|
|
||||
| **Token metrics** | TPM/RPM per konsument, API, modell | Application Insights, Azure Monitor |
|
||||
| **Request logs** | Prompts, completions, latens | App Insights, Log Analytics |
|
||||
| **Built-in dashboard** | Visuell oversikt over AI API-forbruk | APIM portal |
|
||||
| **Custom alerts** | Varsling ved kvote-overskridelse | Azure Monitor Alerts |
|
||||
|
||||
---
|
||||
|
||||
## Bicep-deployment: AI Gateway
|
||||
|
||||
### Grunnleggende APIM-instans for AI Gateway
|
||||
|
||||
```bicep
|
||||
@description('Azure API Management instance for AI Gateway')
|
||||
resource apim 'Microsoft.ApiManagement/service@2023-09-01-preview' = {
|
||||
name: 'apim-ai-gateway-${environment}'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'StandardV2'
|
||||
capacity: 1
|
||||
}
|
||||
identity: {
|
||||
type: 'SystemAssigned'
|
||||
}
|
||||
properties: {
|
||||
publisherEmail: 'admin@example.no'
|
||||
publisherName: 'Statens AI Gateway'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Backend for Azure OpenAI
|
||||
|
||||
```bicep
|
||||
resource openaiBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-norwayeast'
|
||||
properties: {
|
||||
url: 'https://my-aoai-norwayeast.openai.azure.com/openai'
|
||||
protocol: 'http'
|
||||
credentials: {
|
||||
header: {}
|
||||
query: {}
|
||||
}
|
||||
tls: {
|
||||
validateCertificateChain: true
|
||||
validateCertificateName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rolletildeling for Managed Identity
|
||||
|
||||
```bicep
|
||||
@description('Grant APIM Managed Identity access to Azure OpenAI')
|
||||
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
||||
scope: openaiResource
|
||||
name: guid(apim.id, openaiResource.id, cognitiveServicesUserRole)
|
||||
properties: {
|
||||
roleDefinitionId: cognitiveServicesUserRole
|
||||
principalId: apim.identity.principalId
|
||||
principalType: 'ServicePrincipal'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Policy-pipeline for AI Gateway
|
||||
|
||||
En komplett AI gateway-policy kombinerer flere policies i riktig rekkefølge:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- 1. Autentisering mot backend via Managed Identity -->
|
||||
<authentication-managed-identity resource="https://cognitiveservices.azure.com/" />
|
||||
|
||||
<!-- 2. Token rate limiting per subscription -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="10000"
|
||||
estimate-prompt-tokens="true"
|
||||
remaining-tokens-variable-name="remainingTokens" />
|
||||
|
||||
<!-- 3. Semantic cache lookup -->
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
ignore-system-messages="true"
|
||||
max-message-count="10">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
|
||||
<!-- 4. Content safety sjekk -->
|
||||
<llm-content-safety backend-id="content-safety-backend">
|
||||
<categories output-type="EightSeverityLevels">
|
||||
<category name="Hate" threshold="4" />
|
||||
<category name="Violence" threshold="4" />
|
||||
</categories>
|
||||
</llm-content-safety>
|
||||
|
||||
<!-- 5. Ruting til backend pool -->
|
||||
<set-backend-service backend-id="openai-backend-pool" />
|
||||
</inbound>
|
||||
|
||||
<outbound>
|
||||
<base />
|
||||
|
||||
<!-- 6. Lagre i semantic cache -->
|
||||
<azure-openai-semantic-cache-store duration="3600" />
|
||||
|
||||
<!-- 7. Emit token metrikk -->
|
||||
<llm-emit-token-metric namespace="ai-gateway">
|
||||
<dimension name="SubscriptionId"
|
||||
value="@(context.Subscription.Id)" />
|
||||
<dimension name="ApiId"
|
||||
value="@(context.Api.Id)" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relevante referansearkitekturer
|
||||
|
||||
| Ressurs | Beskrivelse | URL |
|
||||
|---------|-------------|-----|
|
||||
| **GenAI Gateway Capabilities** | Oversikt over AI gateway i APIM | learn.microsoft.com/azure/api-management/genai-gateway-capabilities |
|
||||
| **GenAI Gateway Reference Architecture** | Referansearkitektur med APIM | learn.microsoft.com/ai/playbook/technology-guidance/generative-ai/dev-starters/genai-gateway/reference-architectures/apim-based |
|
||||
| **Multi-backend Gateway** | Gateway foran flere Azure OpenAI-instanser | learn.microsoft.com/azure/architecture/ai-ml/guide/azure-openai-gateway-multi-backend |
|
||||
| **GenAI Gateway Toolkit** | Sample policies og lasttesting | github.com/Azure-Samples/apim-genai-gateway-toolkit |
|
||||
| **AI Hub Gateway Accelerator** | Landing zone accelerator | github.com/Azure-Samples/ai-hub-gateway-solution-accelerator |
|
||||
| **Well-Architected Guide for APIM** | WAF service guide | learn.microsoft.com/azure/well-architected/service-guides/api-management/reliability |
|
||||
|
||||
---
|
||||
|
||||
## Hensyn for norsk offentlig sektor
|
||||
|
||||
| Krav | APIM-løsning |
|
||||
|------|---------------|
|
||||
| **Datasuverenitet** | Deploy i Norway East / Sweden Central, private endpoints |
|
||||
| **Schrems II** | Managed Identity eliminerer API-nøkler, data forblir i EU |
|
||||
| **Kostnadsfordeling** | Token metrics per etat/prosjekt via subscriptions og custom headers |
|
||||
| **Tilgangsstyring** | Entra ID-integrasjon, RBAC på APIM-nivå |
|
||||
| **Logging/revisjon** | Diagnostic settings til Log Analytics, retention per regelverk |
|
||||
| **NSM-krav** | VNet-integrasjon, private endpoints, WAF foran APIM |
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- APIM som AI Gateway er den anbefalte PaaS-tilnærmingen for organisasjoner som trenger sentralisert styring over Azure OpenAI og andre LLM-backends -- spesielt relevant for offentlig sektor med krav til kostnadsfordeling og compliance.
|
||||
- De fem hovdkapabilitetene (token limiting, load balancing, circuit breaker, semantic caching, content safety) dekker de fleste enterprise-behov uten egenutviklet kode.
|
||||
- For norsk offentlig sektor: anbefal Standard v2 eller Premium tier, Managed Identity for autentisering, private endpoints, og token-metrikk med dimensjoner per etat/prosjekt for kostnadsallokering.
|
||||
- Policy-pipeline-rekkefølgen er kritisk: autentisering -> token limit -> cache lookup -> content safety -> backend routing (inbound), cache store -> emit metrics (outbound).
|
||||
- Multi-region deployment med APIM Premium gir active-active gateway med innebygd FQDN-routing, men vær oppmerksom på datasuverenitet ved cross-region trafikk.
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
# APIM Authentication: OAuth, Azure AD & Managed Identity
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Autentisering og autorisering er grunnleggende for å sikre AI-tjenester som eksponeres gjennom Azure API Management. Når organisasjoner bygger ut sin AI-plattform med Azure OpenAI, er det kritisk at kun autoriserte applikasjoner og brukere får tilgang, at API-nøkler ikke lekker, og at tilgang kan spores og revideres. APIM tilbyr flere autentiseringsmekanismer som kan kombineres for defense-in-depth.
|
||||
|
||||
For norsk offentlig sektor er sikker autentisering spesielt viktig gitt krav fra NSM, Datatilsynet og interne sikkerhetspolicyer. Managed identity eliminerer behovet for å håndtere API-nøkler, OAuth 2.0 gir finkornet tilgangskontroll, og sertifikatbasert autentisering tilfredsstiller strenge krav til mutual TLS. Denne referansen dekker alle APIM-autentiseringsmønstre relevant for AI-konsumenter.
|
||||
|
||||
APIM fungerer som et sentralt autentiseringspunkt mellom AI-konsumenter og backend-tjenester. Klienter autentiserer seg mot APIM (via subscription keys, OAuth tokens, eller sertifikater), og APIM autentiserer seg mot Azure OpenAI-backend (via managed identity eller API keys). Dette separerer klient-identitet fra backend-tilgang og gir full kontroll over hvem som bruker hvilke AI-modeller.
|
||||
|
||||
---
|
||||
|
||||
## Azure AD Integration
|
||||
|
||||
### Microsoft Entra ID som Identity Provider
|
||||
|
||||
Microsoft Entra ID (tidligere Azure AD) er den primære identitetsleverandøren for Azure-tjenester og integrerer sømløst med APIM:
|
||||
|
||||
| Integrasjonspunkt | Beskrivelse |
|
||||
|-------------------|-------------|
|
||||
| APIM Developer Portal | Brukerinnlogging via Entra ID |
|
||||
| API-autorisering | JWT-validering av access tokens |
|
||||
| Backend-autentisering | Managed identity mot Azure OpenAI |
|
||||
| RBAC | Rollebasert tilgang til APIM-administrasjon |
|
||||
|
||||
### Registrere App i Microsoft Entra ID
|
||||
|
||||
For å sette opp OAuth 2.0-basert tilgang til AI-APIer:
|
||||
|
||||
```
|
||||
1. Azure Portal → Microsoft Entra ID → App registrations
|
||||
2. "+ New registration"
|
||||
- Name: "AI Gateway API"
|
||||
- Supported account types: "Accounts in this organizational directory only"
|
||||
3. Kopier Application (client) ID og Directory (tenant) ID
|
||||
4. Under "Expose an API":
|
||||
- Set Application ID URI: api://ai-gateway-api
|
||||
- Add scope: "AI.Chat", "AI.Completion", "AI.Embedding"
|
||||
5. Under "App roles":
|
||||
- Add role: "AI.User" (for standard tilgang)
|
||||
- Add role: "AI.Admin" (for admin-operasjoner)
|
||||
```
|
||||
|
||||
### APIM Policy for Azure AD Token-validering
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Valider Azure AD token -->
|
||||
<validate-azure-ad-token tenant-id="{{TENANT_ID}}"
|
||||
header-name="Authorization"
|
||||
failed-validation-httpcode="401"
|
||||
failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
|
||||
<client-application-ids>
|
||||
<application-id>{{CLIENT_APP_ID}}</application-id>
|
||||
</client-application-ids>
|
||||
<audiences>
|
||||
<audience>api://ai-gateway-api</audience>
|
||||
</audiences>
|
||||
<required-claims>
|
||||
<claim name="roles" match="any">
|
||||
<value>AI.User</value>
|
||||
<value>AI.Admin</value>
|
||||
</claim>
|
||||
</required-claims>
|
||||
</validate-azure-ad-token>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### RBAC-roller for Azure OpenAI
|
||||
|
||||
| Rolle | Rettigheter | Bruksområde |
|
||||
|-------|------------|------------|
|
||||
| Cognitive Services OpenAI User | Bruke deployments (chat, completion, embedding) | Applikasjoner og managed identities |
|
||||
| Cognitive Services OpenAI Contributor | Opprette og administrere deployments | CI/CD pipelines |
|
||||
| Cognitive Services Contributor | Full tilgang til ressursen | Administratorer |
|
||||
| Reader | Lese-tilgang | Monitorering og audit |
|
||||
|
||||
---
|
||||
|
||||
## OAuth 2.0 Flows
|
||||
|
||||
### Støttede OAuth 2.0 Flows for AI-APIer
|
||||
|
||||
| Flow | Bruksområde | Anbefalt for |
|
||||
|------|------------|-------------|
|
||||
| Client Credentials | Server-til-server (ingen brukerinteraksjon) | Backend-tjenester, automatiserte pipelines |
|
||||
| Authorization Code + PKCE | Web-applikasjoner med brukerinnlogging | Chat-applikasjoner, brukergrensesnitt |
|
||||
| On-Behalf-Of | Delegert tilgang gjennom mellomtjenester | Orchestratorer, middleware |
|
||||
| Device Code | CLI-verktøy og IoT-enheter | Utviklerverktøy, testing |
|
||||
|
||||
### Client Credentials Flow (Server-til-Server)
|
||||
|
||||
Mest brukt for automatiserte AI-tjenester:
|
||||
|
||||
```bash
|
||||
# Hent token via client credentials
|
||||
curl -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=client_credentials" \
|
||||
-d "client_id=${CLIENT_ID}" \
|
||||
-d "client_secret=${CLIENT_SECRET}" \
|
||||
-d "scope=api://ai-gateway-api/.default"
|
||||
```
|
||||
|
||||
### APIM Policy for OAuth 2.0 Validering
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Valider OAuth 2.0 JWT fra ekstern identity provider -->
|
||||
<validate-jwt header-name="Authorization"
|
||||
failed-validation-httpcode="401"
|
||||
failed-validation-error-message="Unauthorized">
|
||||
<openid-config url="https://login.microsoftonline.com/{{TENANT_ID}}/v2.0/.well-known/openid-configuration" />
|
||||
<issuers>
|
||||
<issuer>https://login.microsoftonline.com/{{TENANT_ID}}/v2.0</issuer>
|
||||
</issuers>
|
||||
<audiences>
|
||||
<audience>api://ai-gateway-api</audience>
|
||||
</audiences>
|
||||
<required-claims>
|
||||
<claim name="scp" match="any">
|
||||
<value>AI.Chat</value>
|
||||
<value>AI.Completion</value>
|
||||
</claim>
|
||||
</required-claims>
|
||||
</validate-jwt>
|
||||
|
||||
<!-- Logg bruker-identitet for audit -->
|
||||
<set-header name="X-User-Id" exists-action="override">
|
||||
<value>@(context.Request.Headers.GetValueOrDefault("Authorization","")
|
||||
.AsJwt()?.Claims.GetValueOrDefault("oid", "unknown"))</value>
|
||||
</set-header>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### Scopes og Granulær Tilgangskontroll
|
||||
|
||||
Definer scopes som mapper til AI-kapabiliteter:
|
||||
|
||||
| Scope | Rettighet | Eksempel |
|
||||
|-------|-----------|---------|
|
||||
| `AI.Chat` | Chat completion-tilgang | Standard chatbot-bruk |
|
||||
| `AI.Completion` | Text completion | Automatisk tekstgenerering |
|
||||
| `AI.Embedding` | Embedding-generering | RAG-pipelines, søk |
|
||||
| `AI.ImageGeneration` | DALL-E bildegenererring | Kreativ innholdsproduksjon |
|
||||
| `AI.Admin` | Full tilgang + admin-operasjoner | Modell-administrasjon |
|
||||
|
||||
---
|
||||
|
||||
## Managed Identity
|
||||
|
||||
### System-Assigned vs User-Assigned Managed Identity
|
||||
|
||||
| Type | Livssyklus | Bruksområde |
|
||||
|------|-----------|------------|
|
||||
| System-assigned | Knyttet til APIM-instansen | Enkel oppsett, én identitet per instans |
|
||||
| User-assigned | Uavhengig Azure-ressurs | Delt identitet, multi-region, forhåndskonfigurasjon |
|
||||
|
||||
### Konfigurere Managed Identity for Azure OpenAI
|
||||
|
||||
**Steg 1: Aktiver managed identity på APIM**
|
||||
|
||||
```bash
|
||||
# Aktiver system-assigned managed identity
|
||||
az apim update \
|
||||
--name ai-gateway-apim \
|
||||
--resource-group rg-apim \
|
||||
--enable-managed-identity true
|
||||
```
|
||||
|
||||
**Steg 2: Tildel Cognitive Services OpenAI User-rolle**
|
||||
|
||||
```bash
|
||||
# Hent APIM identity object ID
|
||||
APIM_IDENTITY=$(az apim show --name ai-gateway-apim --resource-group rg-apim \
|
||||
--query identity.principalId -o tsv)
|
||||
|
||||
# Tildel rolle på Azure OpenAI-ressurs
|
||||
az role assignment create \
|
||||
--role "Cognitive Services OpenAI User" \
|
||||
--assignee-object-id $APIM_IDENTITY \
|
||||
--scope /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{aoai-name}
|
||||
```
|
||||
|
||||
**Steg 3: Konfigurer APIM-policy for managed identity autentisering**
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Hent access token via managed identity -->
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com"
|
||||
output-token-variable-name="managed-id-access-token"
|
||||
ignore-error="false" />
|
||||
|
||||
<!-- Sett Authorization-header med bearer token -->
|
||||
<set-header name="Authorization" exists-action="override">
|
||||
<value>@("Bearer " + (string)context.Variables["managed-id-access-token"])</value>
|
||||
</set-header>
|
||||
|
||||
<!-- Fjern eventuell api-key header for sikkerhet -->
|
||||
<set-header name="api-key" exists-action="delete" />
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### Backend-konfigurasjon med Managed Identity
|
||||
|
||||
Alternativ tilnærming via backend-entitet (anbefalt):
|
||||
|
||||
```bicep
|
||||
resource aoaiBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-backend'
|
||||
properties: {
|
||||
url: 'https://my-aoai.openai.azure.com'
|
||||
protocol: 'http'
|
||||
credentials: {
|
||||
authorization: {
|
||||
scheme: 'Bearer'
|
||||
parameter: 'managed-identity'
|
||||
}
|
||||
}
|
||||
tls: {
|
||||
validateCertificateChain: true
|
||||
validateCertificateName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Merk:** Når du importerer en API direkte fra Microsoft Foundry, konfigurerer APIM automatisk managed identity-autentisering mot backend.
|
||||
|
||||
---
|
||||
|
||||
## Client Certificate Authentication
|
||||
|
||||
### Mutual TLS (mTLS)
|
||||
|
||||
For scenarier der klienter må autentisere seg med sertifikater:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Valider klientsertifikat -->
|
||||
<choose>
|
||||
<when condition="@(context.Request.Certificate == null)">
|
||||
<return-response>
|
||||
<set-status code="403" reason="Forbidden" />
|
||||
<set-body>Client certificate required</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
<when condition="@(!context.Request.Certificate.Verify())">
|
||||
<return-response>
|
||||
<set-status code="403" reason="Forbidden" />
|
||||
<set-body>Invalid client certificate</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
<!-- Valider spesifikt thumbprint -->
|
||||
<when condition="@(context.Request.Certificate.Thumbprint != "{{TRUSTED_CERT_THUMBPRINT}}")">
|
||||
<return-response>
|
||||
<set-status code="403" reason="Forbidden" />
|
||||
<set-body>Untrusted client certificate</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
|
||||
<!-- Logg sertifikatinformasjon for audit -->
|
||||
<set-header name="X-Client-Cert-Subject" exists-action="override">
|
||||
<value>@(context.Request.Certificate.Subject)</value>
|
||||
</set-header>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### CA-sertifikat Validering (v2 Tiers)
|
||||
|
||||
I APIM v2-tiers kan du konfigurere custom CA-sertifikater direkte på backend-entiteten:
|
||||
|
||||
| Valideringsmetode | Bruksområde |
|
||||
|-------------------|------------|
|
||||
| Certificate thumbprint | Eksakt sertifikatmatch |
|
||||
| Subject name + Issuer thumbprint | CA-basert validering |
|
||||
| Certificate chain validation | Full kjede-validering |
|
||||
|
||||
---
|
||||
|
||||
## API Key Rotation
|
||||
|
||||
### Sikker API-nøkkelhåndtering via Named Values
|
||||
|
||||
```xml
|
||||
<!-- Bruk named value (eventuelt Key Vault-referanse) for API-nøkler -->
|
||||
<set-header name="api-key" exists-action="override">
|
||||
<value>{{azure-openai-api-key}}</value>
|
||||
</set-header>
|
||||
```
|
||||
|
||||
### Key Vault-integrasjon for Automatisk Rotasjon
|
||||
|
||||
```
|
||||
1. Opprett Key Vault secret med API-nøkkel
|
||||
2. Konfigurer rotasjonspolicy i Key Vault
|
||||
3. Opprett named value i APIM med Key Vault-referanse
|
||||
4. APIM henter automatisk oppdatert nøkkel
|
||||
```
|
||||
|
||||
```bash
|
||||
# Opprett Key Vault secret
|
||||
az keyvault secret set \
|
||||
--vault-name kv-ai-gateway \
|
||||
--name aoai-api-key \
|
||||
--value "your-api-key-here"
|
||||
|
||||
# Opprett APIM named value med Key Vault-referanse
|
||||
az apim nv create \
|
||||
--resource-group rg-apim \
|
||||
--service-name ai-gateway-apim \
|
||||
--named-value-id aoai-api-key \
|
||||
--display-name "Azure OpenAI API Key" \
|
||||
--secret true \
|
||||
--value "your-api-key-here"
|
||||
```
|
||||
|
||||
### Anbefalt Autentiseringshierarki
|
||||
|
||||
| Prioritet | Metode | Sikkerhetsnivå | Anbefalt for |
|
||||
|-----------|--------|---------------|-------------|
|
||||
| 1 | Managed Identity + OAuth 2.0 | Høyest | Produksjonsmiljøer |
|
||||
| 2 | Managed Identity alene | Høy | Enklere oppsett |
|
||||
| 3 | API Key via Key Vault | Moderat | Legacy-integrasjoner |
|
||||
| 4 | API Key direkte | Lavest | Kun dev/test |
|
||||
|
||||
---
|
||||
|
||||
## Defense-in-Depth Mønster
|
||||
|
||||
### Kombinert Autentiseringspolicy
|
||||
|
||||
Kombiner klient-autentisering (OAuth) med backend-autentisering (managed identity):
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- Lag 1: Valider klient-token (OAuth 2.0) -->
|
||||
<validate-azure-ad-token tenant-id="{{TENANT_ID}}"
|
||||
header-name="Authorization"
|
||||
failed-validation-httpcode="401">
|
||||
<client-application-ids>
|
||||
<application-id>{{CLIENT_APP_ID}}</application-id>
|
||||
</client-application-ids>
|
||||
<audiences>
|
||||
<audience>api://ai-gateway-api</audience>
|
||||
</audiences>
|
||||
</validate-azure-ad-token>
|
||||
|
||||
<!-- Lag 2: Ekstraher brukerinfo for audit og rate limiting -->
|
||||
<set-variable name="caller-id"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("Authorization","")
|
||||
.AsJwt()?.Claims.GetValueOrDefault("oid", "anonymous"))" />
|
||||
|
||||
<!-- Lag 3: Rate limiting per bruker -->
|
||||
<llm-token-limit counter-key="@((string)context.Variables["caller-id"])"
|
||||
tokens-per-minute="10000"
|
||||
estimate-prompt-tokens="true" />
|
||||
|
||||
<!-- Lag 4: Autentiser mot backend via managed identity -->
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com"
|
||||
output-token-variable-name="mi-token" />
|
||||
<set-header name="Authorization" exists-action="override">
|
||||
<value>@("Bearer " + (string)context.Variables["mi-token"])</value>
|
||||
</set-header>
|
||||
|
||||
<!-- Fjern klient-nøkler som ikke skal videresendes -->
|
||||
<set-header name="api-key" exists-action="delete" />
|
||||
</inbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Authenticate and authorize access to LLM APIs](https://learn.microsoft.com/en-us/azure/api-management/api-management-authenticate-authorize-ai-apis) — Offisiell autentiseringsguide for AI-APIer i APIM
|
||||
- [Provide custom authentication to Azure OpenAI through a gateway](https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/azure-openai-gateway-custom-authentication) — Arkitekturmønstre for autentisering
|
||||
- [Backends in API Management](https://learn.microsoft.com/en-us/azure/api-management/backends) — Backend-konfigurasjon med managed identity
|
||||
- [Import an Azure OpenAI API](https://learn.microsoft.com/en-us/azure/api-management/azure-openai-api-from-specification) — Automatisk autentiseringsoppsett ved import
|
||||
- [validate-azure-ad-token policy](https://learn.microsoft.com/en-us/azure/api-management/validate-azure-ad-token-policy) — Policy-referanse for Azure AD-tokenvalidering
|
||||
- [authentication-managed-identity policy](https://learn.microsoft.com/en-us/azure/api-management/authentication-managed-identity-policy) — Policy-referanse for managed identity
|
||||
- [How to use managed identities in API Management](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-use-managed-service-identity) — Oppsettguide for managed identity
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunder trenger veiledning om autentisering og autorisering av AI-APIer gjennom APIM, spesielt ved overgang fra API-nøkler til managed identity.
|
||||
- Anbefal alltid managed identity som primær autentiseringsmetode mot Azure OpenAI — det eliminerer nøkkelhåndtering og reduserer angrepsflaten.
|
||||
- For offentlig sektor: Kombiner OAuth 2.0 (klient-autentisering) med managed identity (backend-autentisering) for defense-in-depth. Managed identity alene sikrer kun gateway-til-backend, ikke klient-til-gateway.
|
||||
- Husk at APIM automatisk konfigurerer managed identity-autentisering ved import fra Microsoft Foundry — dette er enkleste oppsett.
|
||||
- Ved multi-region deployment: Sørg for at managed identity har riktige RBAC-roller på alle Azure OpenAI-instanser i alle regioner.
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
# APIM with Azure Front Door for Global AI Distribution
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Nar organisasjoner ruller ut AI-tjenester globalt eller trenger ekstra beskyttelse og ytelsesoptimalisering, er kombinasjonen av Azure Front Door og Azure API Management en kraftig arkitektur. Azure Front Door gir global HTTP(S)-lastbalansering, DDoS-beskyttelse, Web Application Firewall (WAF), edge caching og TLS-offloading -- alt foran APIM som haandterer AI-spesifikk policy-haaandheving, token-ratebegrensning og backend-lastbalansering.
|
||||
|
||||
For norsk offentlig sektor er denne kombinasjonen relevant i flere scenarier: organisasjoner med innbyggertjenester som ma handtere trafikktopper (f.eks. skattemelding-perioden, eksamenssvar), tjenester som eksponeres mot internasjonale brukere, eller organisasjoner som krever ekstra lag med DDoS-beskyttelse. Azure Front Door sine globale PoP-er (Points of Presence) gir lavere latency for brukere narmere edge-lokasjoner.
|
||||
|
||||
Arkitekturen Front Door + APIM + AI Backend gir et tre-lags forsvar: Front Door handterer DDoS og WAF pa nettverksniva, APIM haandterer API-spesifikk sikkerhet og trafikkstyring, og Azure AI-tjenestene handterer modellspesifikk tilgangskontroll. Denne referansen dekker konfigurasjon, sikring og optimalisering av denne arkitekturen.
|
||||
|
||||
---
|
||||
|
||||
## Global lastdistribusjon
|
||||
|
||||
### Arkitekturoversikt
|
||||
|
||||
```
|
||||
Brukere (globalt)
|
||||
|
|
||||
v
|
||||
Azure Front Door (Global L7 Load Balancer)
|
||||
|-- PoP Oslo (naermest norske brukere)
|
||||
|-- PoP London
|
||||
|-- PoP New York
|
||||
|
|
||||
v
|
||||
APIM Instance(r)
|
||||
|-- West Europe (primaer)
|
||||
|-- North Europe (sekundaer)
|
||||
|
|
||||
v
|
||||
AI Backends
|
||||
|-- Azure OpenAI (West Europe)
|
||||
|-- Azure OpenAI (Sweden Central)
|
||||
|-- Microsoft Foundry (North Europe)
|
||||
```
|
||||
|
||||
### Oppsett av Front Door-profil
|
||||
|
||||
Konfigurer Front Door med APIM som origin:
|
||||
|
||||
| Innstilling | Verdi |
|
||||
|------------|-------|
|
||||
| **Origin type** | API Management |
|
||||
| **Origin hostname** | `{apim-name}.azure-api.net` |
|
||||
| **Caching** | Enable caching (for GET-requests) |
|
||||
| **Query string behavior** | Use Query String |
|
||||
| **Health probe path** | `/status-0123456789abcdef` |
|
||||
| **Health probe protocol** | HTTPS |
|
||||
| **Health probe method** | GET |
|
||||
| **Probe interval** | 30 sekunder |
|
||||
|
||||
### Bicep: Front Door med APIM Origin
|
||||
|
||||
```bicep
|
||||
resource frontDoorProfile 'Microsoft.Cdn/profiles@2024-02-01' = {
|
||||
name: frontDoorName
|
||||
location: 'global'
|
||||
sku: {
|
||||
name: 'Premium_AzureFrontDoor' // Premium for Private Link + WAF
|
||||
}
|
||||
}
|
||||
|
||||
resource frontDoorEndpoint 'Microsoft.Cdn/profiles/afdEndpoints@2024-02-01' = {
|
||||
parent: frontDoorProfile
|
||||
name: 'ai-gateway-endpoint'
|
||||
location: 'global'
|
||||
properties: {
|
||||
enabledState: 'Enabled'
|
||||
}
|
||||
}
|
||||
|
||||
resource originGroup 'Microsoft.Cdn/profiles/originGroups@2024-02-01' = {
|
||||
parent: frontDoorProfile
|
||||
name: 'apim-origin-group'
|
||||
properties: {
|
||||
loadBalancingSettings: {
|
||||
sampleSize: 4
|
||||
successfulSamplesRequired: 3
|
||||
additionalLatencyInMilliseconds: 50
|
||||
}
|
||||
healthProbeSettings: {
|
||||
probePath: '/status-0123456789abcdef'
|
||||
probeRequestType: 'GET'
|
||||
probeProtocol: 'Https'
|
||||
probeIntervalInSeconds: 30
|
||||
}
|
||||
sessionAffinityState: 'Disabled'
|
||||
}
|
||||
}
|
||||
|
||||
resource originWestEurope 'Microsoft.Cdn/profiles/originGroups/origins@2024-02-01' = {
|
||||
parent: originGroup
|
||||
name: 'apim-west-europe'
|
||||
properties: {
|
||||
hostName: '${apimNameWestEurope}.azure-api.net'
|
||||
httpPort: 80
|
||||
httpsPort: 443
|
||||
originHostHeader: '${apimNameWestEurope}.azure-api.net'
|
||||
priority: 1
|
||||
weight: 1000
|
||||
enabledState: 'Enabled'
|
||||
enforceCertificateNameCheck: true
|
||||
}
|
||||
}
|
||||
|
||||
resource originNorthEurope 'Microsoft.Cdn/profiles/originGroups/origins@2024-02-01' = {
|
||||
parent: originGroup
|
||||
name: 'apim-north-europe'
|
||||
properties: {
|
||||
hostName: '${apimNameNorthEurope}.azure-api.net'
|
||||
httpPort: 80
|
||||
httpsPort: 443
|
||||
originHostHeader: '${apimNameNorthEurope}.azure-api.net'
|
||||
priority: 2 // Failover
|
||||
weight: 1000
|
||||
enabledState: 'Enabled'
|
||||
enforceCertificateNameCheck: true
|
||||
}
|
||||
}
|
||||
|
||||
resource route 'Microsoft.Cdn/profiles/afdEndpoints/routes@2024-02-01' = {
|
||||
parent: frontDoorEndpoint
|
||||
name: 'ai-gateway-route'
|
||||
properties: {
|
||||
originGroup: {
|
||||
id: originGroup.id
|
||||
}
|
||||
supportedProtocols: [ 'Https' ]
|
||||
patternsToMatch: [ '/ai/*' ]
|
||||
forwardingProtocol: 'HttpsOnly'
|
||||
httpsRedirect: 'Enabled'
|
||||
linkToDefaultDomain: 'Enabled'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DDoS-beskyttelse
|
||||
|
||||
### Front Door innebygd DDoS-beskyttelse
|
||||
|
||||
Azure Front Door gir plattform-niva DDoS-beskyttelse automatisk:
|
||||
|
||||
| Beskyttelsestype | Dekning |
|
||||
|-----------------|---------|
|
||||
| L3/L4 DDoS | Automatisk for alle Front Door-profiler |
|
||||
| L7 DDoS | Via WAF-policyer |
|
||||
| Volumetriske angrep | Absorberes av Front Doors globale nettverk |
|
||||
| Protocol-angrep | Filtreres pa edge |
|
||||
| Application-layer | WAF rate limiting + bot protection |
|
||||
|
||||
### Kombinert beskyttelse
|
||||
|
||||
For kritiske AI-tjenester, kombiner Front Door med Azure DDoS Protection:
|
||||
|
||||
```bicep
|
||||
resource ddosProtectionPlan 'Microsoft.Network/ddosProtectionPlans@2023-11-01' = {
|
||||
name: 'ai-gateway-ddos-plan'
|
||||
location: location
|
||||
properties: {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Web Application Firewall
|
||||
|
||||
### WAF-policy for AI Gateway
|
||||
|
||||
```bicep
|
||||
resource wafPolicy 'Microsoft.Network/FrontDoorWebApplicationFirewallPolicies@2024-02-01' = {
|
||||
name: 'aiGatewayWafPolicy'
|
||||
location: 'global'
|
||||
sku: {
|
||||
name: 'Premium_AzureFrontDoor'
|
||||
}
|
||||
properties: {
|
||||
policySettings: {
|
||||
enabledState: 'Enabled'
|
||||
mode: 'Prevention'
|
||||
requestBodyCheck: 'Enabled'
|
||||
requestBodyInspectLimitInKB: 128
|
||||
}
|
||||
managedRules: {
|
||||
managedRuleSets: [
|
||||
{
|
||||
ruleSetType: 'Microsoft_DefaultRuleSet'
|
||||
ruleSetVersion: '2.1'
|
||||
ruleGroupOverrides: []
|
||||
}
|
||||
{
|
||||
ruleSetType: 'Microsoft_BotManagerRuleSet'
|
||||
ruleSetVersion: '1.1'
|
||||
}
|
||||
]
|
||||
}
|
||||
customRules: {
|
||||
rules: [
|
||||
{
|
||||
name: 'RateLimitAiRequests'
|
||||
priority: 100
|
||||
enabledState: 'Enabled'
|
||||
ruleType: 'RateLimitRule'
|
||||
rateLimitDurationInMinutes: 1
|
||||
rateLimitThreshold: 100
|
||||
matchConditions: [
|
||||
{
|
||||
matchVariable: 'RequestUri'
|
||||
operator: 'Contains'
|
||||
matchValue: [ '/ai/' ]
|
||||
}
|
||||
]
|
||||
action: 'Block'
|
||||
}
|
||||
{
|
||||
name: 'BlockSuspiciousPayloads'
|
||||
priority: 200
|
||||
enabledState: 'Enabled'
|
||||
ruleType: 'MatchRule'
|
||||
matchConditions: [
|
||||
{
|
||||
matchVariable: 'RequestBody'
|
||||
operator: 'Contains'
|
||||
matchValue: [
|
||||
'ignore previous instructions'
|
||||
'ignore all instructions'
|
||||
'disregard your system prompt'
|
||||
]
|
||||
transforms: [ 'Lowercase' ]
|
||||
}
|
||||
]
|
||||
action: 'Block'
|
||||
}
|
||||
{
|
||||
name: 'GeoBlockNonAllowed'
|
||||
priority: 300
|
||||
enabledState: 'Enabled'
|
||||
ruleType: 'MatchRule'
|
||||
matchConditions: [
|
||||
{
|
||||
matchVariable: 'RemoteAddr'
|
||||
operator: 'GeoMatch'
|
||||
negateCondition: true
|
||||
matchValue: [ 'NO', 'SE', 'DK', 'FI' ] // Nordiske land
|
||||
}
|
||||
]
|
||||
action: 'Log' // Start med Log, bytt til Block etter validering
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WAF-regler tilpasset AI-trafikk
|
||||
|
||||
| Regel | Type | Handling | Formal |
|
||||
|-------|------|---------|--------|
|
||||
| Rate limit per IP | RateLimit | Block | Maks 100 req/min per IP |
|
||||
| Prompt injection patterns | Match | Block | Blokkerer kjente injeksjonsmonstre |
|
||||
| Geo-filtering | Match | Log/Block | Begrens til tillatte land |
|
||||
| Bot protection | Managed | Block | Identifiserer og blokkerer botter |
|
||||
| OWASP Core Rules | Managed | Block | Standard webapplikasjonsbeskyttelse |
|
||||
| Payload size limit | Match | Block | Maks request body-storrelse |
|
||||
|
||||
---
|
||||
|
||||
## Edge Caching
|
||||
|
||||
### Caching-strategi for AI med Front Door
|
||||
|
||||
For AI-API-er er caching begrenset til GET-requests og statisk innhold. POST-baserte chat completion-kall caches ikke av Front Door, men det finnes bruksomrader:
|
||||
|
||||
| Innholdstype | Cachebar? | Strategi |
|
||||
|-------------|-----------|----------|
|
||||
| Chat completions (POST) | Nei | Bruk APIM semantisk caching |
|
||||
| Model listing (GET) | Ja | Front Door edge cache |
|
||||
| API documentation | Ja | Front Door edge cache |
|
||||
| Health endpoints | Nei | Bypass cache |
|
||||
| Static assets (dev portal) | Ja | Front Door edge cache |
|
||||
|
||||
### Caching for Developer Portal
|
||||
|
||||
```bicep
|
||||
resource devPortalRoute 'Microsoft.Cdn/profiles/afdEndpoints/routes@2024-02-01' = {
|
||||
parent: frontDoorEndpoint
|
||||
name: 'dev-portal-route'
|
||||
properties: {
|
||||
originGroup: {
|
||||
id: devPortalOriginGroup.id
|
||||
}
|
||||
supportedProtocols: [ 'Https' ]
|
||||
patternsToMatch: [ '/developer/*' ]
|
||||
forwardingProtocol: 'HttpsOnly'
|
||||
cacheConfiguration: {
|
||||
queryStringCachingBehavior: 'IgnoreQueryString'
|
||||
compressionSettings: {
|
||||
isCompressionEnabled: true
|
||||
contentTypesToCompress: [
|
||||
'text/html'
|
||||
'text/css'
|
||||
'application/javascript'
|
||||
'application/json'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Geografisk ruting
|
||||
|
||||
### Priority-basert ruting med failover
|
||||
|
||||
```
|
||||
Bruker i Norge
|
||||
|
|
||||
v
|
||||
Front Door PoP Oslo
|
||||
|
|
||||
|-- Priority 1: APIM West Europe (Nederland)
|
||||
|-- Priority 2: APIM North Europe (Irland)
|
||||
|
|
||||
v
|
||||
APIM West Europe
|
||||
|
|
||||
|-- Priority 1: Azure OpenAI Sweden Central
|
||||
|-- Priority 2: Azure OpenAI West Europe
|
||||
```
|
||||
|
||||
### Restriksjon: Kun Front Door-trafikk til APIM
|
||||
|
||||
Sikre at APIM kun aksepterer trafikk fra Front Door:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Verify Front Door ID header -->
|
||||
<check-header name="X-Azure-FDID"
|
||||
failed-check-httpcode="403"
|
||||
failed-check-error-message="Access denied. Traffic must route through Azure Front Door."
|
||||
ignore-case="false">
|
||||
<value>{{FrontDoorId}}</value>
|
||||
</check-header>
|
||||
|
||||
<!-- Additionally restrict to Front Door IP ranges -->
|
||||
<ip-filter action="allow">
|
||||
<address-range from="147.243.0.0" to="147.243.255.255" />
|
||||
<!-- AzureFrontDoor.Backend service tag ranges -->
|
||||
</ip-filter>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Private Link mellom Front Door og APIM
|
||||
|
||||
For maksimal sikkerhet, bruk Front Door Premium med Private Link:
|
||||
|
||||
```bicep
|
||||
resource privateEndpoint 'Microsoft.Cdn/profiles/originGroups/origins@2024-02-01' = {
|
||||
parent: originGroup
|
||||
name: 'apim-private'
|
||||
properties: {
|
||||
hostName: '${apimName}.azure-api.net'
|
||||
originHostHeader: '${apimName}.azure-api.net'
|
||||
priority: 1
|
||||
weight: 1000
|
||||
enabledState: 'Enabled'
|
||||
sharedPrivateLinkResource: {
|
||||
privateLink: {
|
||||
id: apiManagement.id
|
||||
}
|
||||
privateLinkLocation: location
|
||||
groupId: 'gateway'
|
||||
requestMessage: 'Front Door Private Link to APIM'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kostnadsestimat for Front Door + APIM
|
||||
|
||||
| Komponent | Manedlig kostnad (NOK) |
|
||||
|-----------|----------------------|
|
||||
| Front Door Premium (base) | ~4 000 |
|
||||
| Front Door: 10M requests | ~1 500 |
|
||||
| Front Door: Data transfer (100 GB) | ~1 000 |
|
||||
| WAF Policy (Premium) | ~4 500 |
|
||||
| APIM Standard v2 | ~20 000 |
|
||||
| **Total** | **~31 000** |
|
||||
|
||||
Merk: Front Door Standard er rimeligere (~60% av Premium-pris) men mangler Private Link og WAF Managed Rules.
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Configure Front Door Standard/Premium in front of Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/front-door-api-management) -- trinnvis veiledning
|
||||
- [What is Azure Front Door?](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-overview) -- oversikt
|
||||
- [Azure Front Door DDoS protection](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-ddos) -- DDoS-beskyttelse
|
||||
- [Web Application Firewall on Azure Front Door](https://learn.microsoft.com/en-us/azure/web-application-firewall/afds/afds-overview) -- WAF-oversikt
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) -- AI gateway
|
||||
- [Restrict caller IPs policy](https://learn.microsoft.com/en-us/azure/api-management/ip-filter-policy) -- IP-filtrering
|
||||
- [Check header policy](https://learn.microsoft.com/en-us/azure/api-management/check-header-policy) -- header-validering
|
||||
- [Architecture best practices for Azure Front Door](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-front-door) -- Well-Architected-anbefalinger
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** nar kunden trenger global distribusjon av AI-tjenester, ekstra DDoS-beskyttelse, eller WAF foran AI-gateway-en sin.
|
||||
- For de fleste norske offentlige virksomheter er Front Door overkill for rene interne AI-tjenester. Anbefal det primaert for innbyggerrettede tjenester med hoy trafikk eller behov for geographic redundancy.
|
||||
- Front Door Premium er nodvendig for Private Link til APIM og WAF Managed Rules. Standard-tier mangler disse, men er tilstrekkelig for basic lastbalansering og DDoS.
|
||||
- Husk alltid a konfigurere `X-Azure-FDID`-header-sjekk i APIM for a forhindre at noen omgar Front Door og kaller APIM direkte.
|
||||
- Kombiner Front Door WAF-regler for prompt injection-monstre pa nettverksniva med APIM Content Safety policy for AI-spesifikk innholdsmoderasjon -- dette gir forsvar i dybden.
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
# APIM vs Direct Access: Trade-offs & Decision Matrix
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
En av de første arkitekturbeslutningene ved implementering av Azure OpenAI er om applikasjoner skal koble seg direkte til Azure OpenAI-endepunktene, eller om trafikken skal gå gjennom en gateway som Azure API Management. Svaret avhenger av organisasjonens størrelse, sikkerhetskrav, antall applikasjoner og modelldeployments, samt behovet for sentral styring og observerbarhet.
|
||||
|
||||
For norsk offentlig sektor, der sikkerhet, governance, transparens og kostnadseffektivitet er sentrale verdier, er gateway-tilnærmingen typisk å foretrekke. Men for enklere piloter og enkelt-applikasjon-scenarier kan direkte tilgang være tilstrekkelig. Denne referansen gir en systematisk sammenligning for å hjelpe med beslutningen.
|
||||
|
||||
Azure Well-Architected Framework identifiserer utfordringer ved direkte tilgang på tvers av alle fem pilarer: sikkerhet, pålitelighet, ytelse, kostnadseffektivitet og operasjonell dyktighet. En gateway adresserer de fleste av disse, men introduserer også ny kompleksitet og kostnader. Riktig valg krever en helhetsvurdering.
|
||||
|
||||
---
|
||||
|
||||
## Gateway Overhead Analysis
|
||||
|
||||
### Latensoverhead
|
||||
|
||||
APIM legger til en liten latens for policy-kjøring og nettverkshopping:
|
||||
|
||||
| Scenario | Direkte tilgang | Via APIM | Overhead |
|
||||
|----------|----------------|----------|----------|
|
||||
| Enkel chat completion | ~200ms | ~210-230ms | +10-30ms |
|
||||
| Med autentisering + rate limiting | N/A | ~220-250ms | +20-50ms |
|
||||
| Med content safety | N/A | ~300-500ms | +100-300ms |
|
||||
| Med semantic caching (hit) | ~200ms | ~50-100ms | -100-150ms (raskere!) |
|
||||
| Streaming (time-to-first-token) | ~100ms | ~110-130ms | +10-30ms |
|
||||
|
||||
**Merk:** Semantic caching kan redusere latens betydelig ved gjentatte lignende spørsmål.
|
||||
|
||||
### Throughput
|
||||
|
||||
| APIM Tier | Scale Units | Estimert RPS | Kostnad/mnd (NOK) |
|
||||
|-----------|------------|-------------|-------------------|
|
||||
| Standard v2 | 1 | ~1000 | ~2,500 |
|
||||
| Premium | 1 | ~2500 | ~25,000 |
|
||||
| Premium | 2 (multi-region) | ~5000 | ~50,000 |
|
||||
|
||||
### Ressursforbruk
|
||||
|
||||
| Ressurs | Direkte | Via APIM |
|
||||
|---------|---------|----------|
|
||||
| Nettverkshopp | 1 (klient→AOAI) | 2 (klient→APIM→AOAI) |
|
||||
| DNS-oppslag | 1 | 2 |
|
||||
| TLS-handshake | 1 | 2 (med connection pooling: ~1.1) |
|
||||
| CPU (gateway) | 0 | APIM policy-kjøring |
|
||||
| Minne | 0 | APIM caching, policy state |
|
||||
|
||||
---
|
||||
|
||||
## Security Posture Comparison
|
||||
|
||||
### Sikkerhetsfunksjoner
|
||||
|
||||
| Sikkerhetsfunksjon | Direkte tilgang | Via APIM |
|
||||
|-------------------|----------------|----------|
|
||||
| API-nøkkelhåndtering | Klient har nøkkel | Nøkkel skjult i APIM |
|
||||
| Managed identity | Klient trenger MI | APIM MI (sentralisert) |
|
||||
| OAuth 2.0 validering | Custom kode | Innebygd policy |
|
||||
| Rate limiting | Kun AOAI-kvoter | Granulær per bruker/app |
|
||||
| IP-filtrering | NSG/Firewall | APIM policy + NSG |
|
||||
| Content Safety | Custom integrasjon | Innebygd policy |
|
||||
| Prompt Shield | Custom integrasjon | Innebygd policy |
|
||||
| mTLS | Custom oppsett | Innebygd støtte |
|
||||
| Audit logging | Custom logging | Innebygd diagnostikk |
|
||||
| Key rotation | Manuell per app | Sentralisert via Key Vault |
|
||||
|
||||
### Angrepsflate
|
||||
|
||||
```
|
||||
Direkte tilgang:
|
||||
Klient ←→ Azure OpenAI
|
||||
- API-nøkkel eksponert i klientkonfigurasjon
|
||||
- Ingen sentral policy-håndhevelse
|
||||
- Vanskelig å rotere nøkler på tvers av applikasjoner
|
||||
- Ingen prompt-validering
|
||||
|
||||
Via APIM:
|
||||
Klient ←→ APIM Gateway ←→ Azure OpenAI
|
||||
- API-nøkkel kun i APIM (eller managed identity)
|
||||
- Sentral autentisering og autorisering
|
||||
- Enkel nøkkelrotasjon
|
||||
- Content Safety og Prompt Shield integrert
|
||||
- Full audit trail
|
||||
```
|
||||
|
||||
### NSM Grunnprinsipper-mapping
|
||||
|
||||
| NSM Prinsipp | Direkte | APIM |
|
||||
|-------------|---------|------|
|
||||
| Identifisere og kartlegge | Manuell per app | Sentralt API-register |
|
||||
| Beskytte og opprettholde | Per-app sikkerhet | Sentrale policyer |
|
||||
| Oppdage | Custom logging | Innebygd observerbarhet |
|
||||
| Håndtere og gjenopprette | Per-app | Sentralt med circuit breaker |
|
||||
|
||||
---
|
||||
|
||||
## Governance Requirements
|
||||
|
||||
### Governance-kapabiliteter
|
||||
|
||||
| Kapabilitet | Direkte tilgang | Via APIM |
|
||||
|------------|----------------|----------|
|
||||
| API-versjonering | Manuelt per app | Sentralisert |
|
||||
| Policy enforcement | Ingen | Innebygd |
|
||||
| Token-kvoter per team | Ikke mulig | llm-token-limit policy |
|
||||
| Modell-tilgangskontroll | RBAC per AOAI | APIM Products + subscriptions |
|
||||
| Usage tracking | AOAI-metriker | Detaljerte APIM-metriker |
|
||||
| Chargeback | Ikke mulig | Innebygde dimensjoner |
|
||||
| Compliance reporting | Custom | Innebygd dashboard |
|
||||
| Developer portal | Ikke aktuelt | Innebygd self-service |
|
||||
|
||||
### Governance-scenario: Multi-Team AI Platform
|
||||
|
||||
```
|
||||
Uten APIM:
|
||||
Team A → AOAI Endpoint 1 (egne nøkler, egen logging)
|
||||
Team B → AOAI Endpoint 1 (egne nøkler, egen logging)
|
||||
Team C → AOAI Endpoint 2 (egne nøkler, egen logging)
|
||||
→ Ingen sentral oversikt, ingen policy-kontroll
|
||||
|
||||
Med APIM:
|
||||
Team A → APIM (Subscription A, Product: AI-Standard)
|
||||
Team B → APIM (Subscription B, Product: AI-Premium)
|
||||
Team C → APIM (Subscription C, Product: AI-Standard)
|
||||
→ Sentral token-kvote, logging, chargeback, content safety
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cost per Request
|
||||
|
||||
### Total Cost of Ownership
|
||||
|
||||
| Kostnadspost | Direkte | Via APIM (Standard v2) | Via APIM (Premium) |
|
||||
|-------------|---------|----------------------|-------------------|
|
||||
| APIM-infrastruktur | 0 | ~2,500 NOK/mnd | ~25,000 NOK/mnd |
|
||||
| Azure OpenAI tokens | Samme | Samme | Samme |
|
||||
| Utviklingskostnad | Høy (per app) | Lav (sentral) | Lav (sentral) |
|
||||
| Drift og vedlikehold | Høy (per app) | Lav (sentral) | Lav (sentral) |
|
||||
| Sikkerhetsimplementasjon | Per app | Inkludert | Inkludert |
|
||||
| Logging-infrastruktur | Custom | Inkludert | Inkludert |
|
||||
| Nøkkelrotasjon | Manuell | Automatisert | Automatisert |
|
||||
|
||||
### Break-even Analyse
|
||||
|
||||
```
|
||||
APIM Standard v2 kost: ~2,500 NOK/mnd
|
||||
|
||||
Estimert besparelse per applikasjon:
|
||||
- Eliminert custom auth-kode: ~2,000 NOK/mnd (drift)
|
||||
- Eliminert custom logging: ~1,000 NOK/mnd (drift)
|
||||
- Redusert sikkerhetsinnsats: ~1,500 NOK/mnd
|
||||
- Semantic caching token-besparelse: variabel
|
||||
|
||||
Break-even: ~1 applikasjon for Standard v2
|
||||
~6 applikasjoner for Premium
|
||||
```
|
||||
|
||||
### Kostnad ved Semantic Caching
|
||||
|
||||
Semantic caching kan redusere Azure OpenAI-kostnader betydelig:
|
||||
|
||||
| Cache Hit Rate | Token-besparelse | Typisk ROI |
|
||||
|---------------|-----------------|-----------|
|
||||
| 10% | ~10% reduksjon | Moderat |
|
||||
| 30% | ~30% reduksjon | God |
|
||||
| 50%+ | ~50%+ reduksjon | Utmerket |
|
||||
|
||||
**Eksempel:** 1M tokens/dag × 0.10 NOK/1K tokens = 100 NOK/dag.
|
||||
Med 30% cache hit: 70 NOK/dag → ~900 NOK besparelse/mnd (dekker Standard v2-kostnad).
|
||||
|
||||
---
|
||||
|
||||
## Organizational Scale Factors
|
||||
|
||||
### Decision Matrix
|
||||
|
||||
| Faktor | Score: Direkte | Score: APIM | Vekt |
|
||||
|--------|---------------|-------------|------|
|
||||
| 1 applikasjon | 5 | 2 | Høy |
|
||||
| 2-5 applikasjoner | 3 | 4 | Høy |
|
||||
| 6+ applikasjoner | 1 | 5 | Høy |
|
||||
| Sikkerhetskrav (standard) | 3 | 4 | Medium |
|
||||
| Sikkerhetskrav (strengt) | 1 | 5 | Høy |
|
||||
| Chargeback-behov | 0 | 5 | Medium |
|
||||
| Multi-team | 1 | 5 | Høy |
|
||||
| Content Safety-krav | 1 | 5 | Høy |
|
||||
| Enkel pilot/POC | 5 | 2 | Lav |
|
||||
| Produksjon | 2 | 5 | Høy |
|
||||
| Compliance-rapportering | 1 | 5 | Medium |
|
||||
|
||||
**Scoring:** 1 = Dårlig egnet, 5 = Svært godt egnet
|
||||
|
||||
### Beslutningstre
|
||||
|
||||
```
|
||||
Spørsmål 1: Kun én applikasjon med lav trafikk?
|
||||
JA → Spørsmål 2: Strenge sikkerhetskrav (offentlig sektor)?
|
||||
JA → APIM (sikkerhet trumfer enkelhet)
|
||||
NEI → Direkte tilgang (POC/pilot)
|
||||
|
||||
NEI → Spørsmål 3: Flere team/avdelinger deler AI?
|
||||
JA → APIM Premium (governance, chargeback)
|
||||
NEI → Spørsmål 4: Behov for multi-region eller failover?
|
||||
JA → APIM Premium (multi-region)
|
||||
NEI → APIM Standard v2 (sentral gateway)
|
||||
```
|
||||
|
||||
### Anbefaling per Organisasjonstype
|
||||
|
||||
| Organisasjon | Anbefaling | Tier | Begrunnelse |
|
||||
|-------------|-----------|------|------------|
|
||||
| Enkelt team, pilot | Direkte tilgang | N/A | Minst friksjon |
|
||||
| Enkelt team, produksjon | APIM Standard v2 | Standard v2 | Sikkerhet + logging |
|
||||
| Flere team, felles AI | APIM Premium | Premium | Governance + chargeback |
|
||||
| Offentlig sektor, produksjon | APIM Premium | Premium | Compliance + multi-region |
|
||||
| Enterprise, multi-region | APIM Premium | Premium | Full kapabilitet |
|
||||
|
||||
---
|
||||
|
||||
## Migrasjonsvei: Direkte → APIM
|
||||
|
||||
### Gradvis Migrasjon
|
||||
|
||||
```
|
||||
Fase 1: Deploy APIM med proxy-modus
|
||||
- Import AOAI API til APIM
|
||||
- Konfigurer managed identity
|
||||
- APIM viderekobler til eksisterende AOAI
|
||||
- Ingen endring i AOAI-konfigurasjon
|
||||
|
||||
Fase 2: Omdirigér applikasjoner
|
||||
- Oppdater endepunkt fra AOAI → APIM
|
||||
- Legg til subscription key
|
||||
- Test per applikasjon
|
||||
- Gradvis utrulling
|
||||
|
||||
Fase 3: Aktiver APIM-policyer
|
||||
- Rate limiting
|
||||
- Authentication (OAuth 2.0)
|
||||
- Token-metriker
|
||||
- Content Safety
|
||||
|
||||
Fase 4: Fjern direkte tilgang
|
||||
- Fjern public endpoint på AOAI
|
||||
- Konfigurer private endpoints
|
||||
- APIM som eneste inngang
|
||||
```
|
||||
|
||||
### Minimal-Endring Policy
|
||||
|
||||
For å starte med minimal påvirkning på eksisterende applikasjoner:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Pass-through: Videresend API-nøkkel fra klient -->
|
||||
<set-backend-service backend-id="aoai-backend" />
|
||||
</inbound>
|
||||
<backend>
|
||||
<forward-request buffer-response="false" />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Start med kun logging -->
|
||||
<llm-emit-token-metric namespace="ai-metrics">
|
||||
<dimension name="API" value="@(context.Api.Name)" />
|
||||
<dimension name="Subscription" value="@(context.Subscription.Name)" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hybrid-tilnærminger
|
||||
|
||||
### APIM for Governance + Direkte for Latens-kritisk
|
||||
|
||||
```
|
||||
Batch-operasjoner → APIM → Azure OpenAI (full policy-stack)
|
||||
Real-time chatbot → APIM → Azure OpenAI (minimal policy)
|
||||
Embedding-pipeline → Direkte → Azure OpenAI (ingen gateway)
|
||||
```
|
||||
|
||||
### Global Standard + APIM
|
||||
|
||||
Azure OpenAI Global Standard deployment med APIM for governance:
|
||||
|
||||
```
|
||||
APIM håndterer: Autentisering, rate limiting, logging
|
||||
AOAI håndterer: Global routing, kapasitetsallokering
|
||||
```
|
||||
|
||||
**Merk:** Global Standard deployments ruter automatisk til regioner med kapasitet — dette er en annen form for load balancing enn APIM backend pools.
|
||||
|
||||
---
|
||||
|
||||
## Well-Architected Framework Perspektiv
|
||||
|
||||
### Sammenligning per WAF-pilar
|
||||
|
||||
| WAF-pilar | Direkte tilgang | Via APIM |
|
||||
|-----------|----------------|----------|
|
||||
| **Reliability** | Failover må implementeres i klientkode | Innebygd backend pools, circuit breaker, multi-region |
|
||||
| **Security** | API-nøkler i klientkonfig, ingen sentral policy | Managed identity, OAuth, Content Safety, sentral policy |
|
||||
| **Cost Optimization** | Ingen synlighet i forbruk per team | Token-metriker, chargeback, semantic caching |
|
||||
| **Operational Excellence** | Logging per applikasjon | Sentralisert diagnostikk, innebygd dashboard |
|
||||
| **Performance Efficiency** | Ingen caching-lag | Semantic caching, regional routing |
|
||||
|
||||
### Utfordringer ved Direkte Tilgang (fra Azure Architecture Center)
|
||||
|
||||
Microsoft identifiserer følgende utfordringer ved direkte tilgang:
|
||||
|
||||
1. **Sikkerhet**: API-nøkler hardkodet eller lagret i klientkonfigurasjon. Ingen sentral mekanisme for nøkkelrotasjon.
|
||||
2. **Pålitelighet**: Klientkode må håndtere throttling (429), failover, og retry-logikk. Ingen automatisk load balancing.
|
||||
3. **Kostnader**: Ingen synlighet i token-forbruk per team/avdeling. Umulig å implementere chargeback.
|
||||
4. **Observerbarhet**: Ingen sentral logging. Vanskelig å spore hvem som bruker hva.
|
||||
5. **Governance**: Ingen policy-håndhevelse. Klienter kan sende vilkårlig innhold til modellen.
|
||||
|
||||
### Scenario-vurdering
|
||||
|
||||
**Scenario 1: Intern chatbot for én avdeling**
|
||||
```
|
||||
Direkte tilgang: Akseptabelt for POC
|
||||
APIM: Anbefalt for produksjon (logging, content safety)
|
||||
Vurdering: Start direkte, migrer til APIM før prod
|
||||
```
|
||||
|
||||
**Scenario 2: AI-plattform for hele organisasjonen**
|
||||
```
|
||||
Direkte tilgang: Ikke anbefalt (ingen governance)
|
||||
APIM: Obligatorisk (chargeback, rate limiting, content safety)
|
||||
Vurdering: APIM Premium fra start
|
||||
```
|
||||
|
||||
**Scenario 3: RAG-pipeline (batch-orientert)**
|
||||
```
|
||||
Direkte tilgang: Akseptabelt (lav latens-krav, enkel arkitektur)
|
||||
APIM: Valgfritt (logging og rate limiting er nyttig)
|
||||
Vurdering: Vurder basert på compliance-krav
|
||||
```
|
||||
|
||||
**Scenario 4: Multi-region med DR-krav**
|
||||
```
|
||||
Direkte tilgang: Svært kompleks (klientbasert failover)
|
||||
APIM: Sterkt anbefalt (innebygd multi-region, FQDN routing)
|
||||
Vurdering: APIM Premium med multi-region deployment
|
||||
```
|
||||
|
||||
### Når Direkte Tilgang er Riktig
|
||||
|
||||
Direkte tilgang kan være riktig valg i følgende scenarier:
|
||||
|
||||
| Scenario | Begrunnelse |
|
||||
|----------|-------------|
|
||||
| POC/Prototype (< 1 mnd) | Minst mulig overhead |
|
||||
| Enkeltapplikasjon, lavt volum | Gateway-overhead urettferdiggjort |
|
||||
| Embedding-pipeline (intern batch) | Ingen brukerinteraksjon, lav risiko |
|
||||
| Dev/test-miljø | Unødvendig å gateway-beskytte testdata |
|
||||
| Global Standard deployment | Innebygd global routing, begrenset APIM-verdi |
|
||||
|
||||
### Når APIM er Obligatorisk
|
||||
|
||||
APIM bør alltid brukes i følgende scenarier:
|
||||
|
||||
| Scenario | Begrunnelse |
|
||||
|----------|-------------|
|
||||
| Offentlig sektor (produksjon) | Compliance, audit, content safety |
|
||||
| Multi-team AI-plattform | Governance, chargeback, rate limiting |
|
||||
| Brukervendte AI-tjenester | Content Safety, prompt shield |
|
||||
| Multi-region deployment | Failover, latency routing |
|
||||
| SLA-krav > 99.9% | Multi-region, circuit breaker |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Access Azure OpenAI and other language models through a gateway](https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/azure-openai-gateway-guide) — Well-Architected Framework gateway-guide
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) — AI gateway-kapabiliteter
|
||||
- [Architecture best practices for Azure API Management](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-api-management) — WAF-guide for APIM
|
||||
- [Use a gateway in front of multiple Azure OpenAI deployments](https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/azure-openai-gateway-multi-backend) — Multi-backend arkitekturmønstre
|
||||
- [Scale OpenAI for Python with Azure API Management](https://learn.microsoft.com/en-us/azure/developer/python/get-started-app-chat-scaling-with-azure-api-management) — Praktisk implementasjonsguide
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunder spør om de trenger APIM eller kan koble direkte til Azure OpenAI. Beslutningsmatrisen gir et strukturert svar.
|
||||
- Tommelfingerregel: Hvis kunden har mer enn én applikasjon ELLER strenge sikkerhetskrav (som offentlig sektor typisk har) → anbefal APIM.
|
||||
- For POC og piloter: Direkte tilgang er OK, men planlegg for gateway fra start — refaktorering fra direkte til APIM er arbeid som kan unngås.
|
||||
- Husk at APIM med semantic caching kan faktisk redusere total kostnad og latens — gateway er ikke bare overhead, det er også ytelsesoptimalisering.
|
||||
- For norsk offentlig sektor er APIM nesten alltid riktig valg: compliance, audit logging, content safety og chargeback er typisk påkrevd.
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
# Backend Pool Management & Health Probes
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Backend pool management i Azure API Management er fundamentalt for å bygge robuste AI-gateways. Når organisasjoner skalerer sin bruk av Azure OpenAI og andre LLM-tjenester, trenger de en mekanisme for å distribuere trafikk på tvers av flere backend-instanser, håndtere throttling gracefully, og sikre at feilende backends ikke påvirker sluttbrukere. APIM backend pools gir nettopp denne kapabiliteten med støtte for round-robin, vektet, prioritetsbasert og session-aware load balancing.
|
||||
|
||||
For norsk offentlig sektor, der AI-tjenester ofte skal være tilgjengelige for mange etater og brukere, er riktig backend pool-konfigurasjon avgjørende. Et typisk mønster er å ha Provisioned Throughput Units (PTU) som prioritert backend med pay-as-you-go Standard-deployments som fallback. Combined med circuit breaker-regler sikrer dette at tjenesten forblir tilgjengelig selv under høy belastning eller partielle feil.
|
||||
|
||||
Denne referansen dekker konfigurasjon av backend-entiteter, opprettelse av load-balanserte pools, circuit breaker-regler, helsesjekker, og timeout/retry-logikk — alt spesifikt for AI-workloads med Azure OpenAI som backend.
|
||||
|
||||
---
|
||||
|
||||
## Backend Configuration
|
||||
|
||||
### Opprette Backend-entiteter
|
||||
|
||||
Hver Azure OpenAI-instans representeres som en backend-entitet i APIM:
|
||||
|
||||
```bicep
|
||||
resource aoaiBackendWestEurope 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-westeurope'
|
||||
properties: {
|
||||
url: 'https://aoai-westeurope.openai.azure.com'
|
||||
protocol: 'http'
|
||||
title: 'Azure OpenAI - West Europe'
|
||||
description: 'PTU deployment i West Europe'
|
||||
credentials: {
|
||||
authorization: {
|
||||
scheme: 'managed-identity'
|
||||
parameter: 'https://cognitiveservices.azure.com'
|
||||
}
|
||||
}
|
||||
tls: {
|
||||
validateCertificateChain: true
|
||||
validateCertificateName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource aoaiBackendNorthEurope 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-northeurope'
|
||||
properties: {
|
||||
url: 'https://aoai-northeurope.openai.azure.com'
|
||||
protocol: 'http'
|
||||
title: 'Azure OpenAI - North Europe'
|
||||
description: 'Standard deployment i North Europe (fallback)'
|
||||
credentials: {
|
||||
authorization: {
|
||||
scheme: 'managed-identity'
|
||||
parameter: 'https://cognitiveservices.azure.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Backend Properties
|
||||
|
||||
| Egenskap | Beskrivelse | Relevans for AI |
|
||||
|----------|-------------|----------------|
|
||||
| `url` | Base URL for backend-tjenesten | Azure OpenAI endpoint |
|
||||
| `protocol` | `http` for REST-backends | Alltid `http` for OpenAI |
|
||||
| `credentials` | Autentiseringsmetode | Managed identity anbefalt |
|
||||
| `circuitBreaker` | Circuit breaker-regler | Håndterer 429 throttling |
|
||||
| `tls` | TLS-valideringinnstillinger | Sertifikatkjedevalidering |
|
||||
|
||||
### Referere til Backend i Policyer
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Direkte backend-referanse -->
|
||||
<set-backend-service backend-id="aoai-westeurope" />
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### Automatisk Backend-deteksjon
|
||||
|
||||
APIM kan automatisk matche requests til backend-entiteter basert på URL. Når en request sendes til en backend-URL som matcher en registrert backend-entitet, brukes denne automatisk — inkludert circuit breaker-regler og credentials.
|
||||
|
||||
---
|
||||
|
||||
## Load-Balanced Backend Pools
|
||||
|
||||
### Pool-typer for AI-workloads
|
||||
|
||||
| Load Balancing | Beskrivelse | AI-bruksscenario |
|
||||
|---------------|-------------|-----------------|
|
||||
| Round-robin | Jevn distribusjon | Likeverdige pay-as-you-go instanser |
|
||||
| Weighted | Vektet distribusjon | Blue-green deployment av modeller |
|
||||
| Priority-based | Prioritetsgrupper | PTU først, Standard som fallback |
|
||||
| Session-aware | Sticky sessions | Chat-assistenter, tråd-baserte samtaler |
|
||||
|
||||
### Priority-basert Pool (PTU + Standard Fallback)
|
||||
|
||||
Det mest brukte mønsteret for AI-workloads:
|
||||
|
||||
```bicep
|
||||
resource aoaiPool 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-pool'
|
||||
properties: {
|
||||
description: 'PTU prioritert med Standard fallback'
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
// PTU deployment - prioritet 1 (brukes først)
|
||||
id: '/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/ai-gateway-apim/backends/aoai-ptu-westeurope'
|
||||
priority: 1
|
||||
weight: 1
|
||||
}
|
||||
{
|
||||
// Standard deployment - prioritet 2 (fallback)
|
||||
id: '/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/ai-gateway-apim/backends/aoai-standard-westeurope'
|
||||
priority: 2
|
||||
weight: 1
|
||||
}
|
||||
{
|
||||
// Standard deployment annen region - prioritet 3 (siste fallback)
|
||||
id: '/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/ai-gateway-apim/backends/aoai-standard-northeurope'
|
||||
priority: 3
|
||||
weight: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Weighted Pool (Blue-Green)
|
||||
|
||||
For gradvis utrulling av ny modellversjon:
|
||||
|
||||
```bicep
|
||||
resource aoaiPoolBlueGreen 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-bluegreen'
|
||||
properties: {
|
||||
description: 'Blue-green deployment for modelloppgradering'
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
// Eksisterende modellversjon (blue) - 90% trafikk
|
||||
id: '.../backends/aoai-gpt4o-v1'
|
||||
priority: 1
|
||||
weight: 9
|
||||
}
|
||||
{
|
||||
// Ny modellversjon (green) - 10% trafikk
|
||||
id: '.../backends/aoai-gpt4o-v2'
|
||||
priority: 1
|
||||
weight: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Session-Aware Pool for Chat-tjenester
|
||||
|
||||
Sikrer at alle requests i en chat-samtale rutes til samme backend:
|
||||
|
||||
```bicep
|
||||
resource aoaiPoolSession 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-chat-pool'
|
||||
properties: {
|
||||
description: 'Session-aware pool for Assistants API'
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
id: '.../backends/aoai-assistant-1'
|
||||
priority: 1
|
||||
weight: 1
|
||||
}
|
||||
{
|
||||
id: '.../backends/aoai-assistant-2'
|
||||
priority: 1
|
||||
weight: 1
|
||||
}
|
||||
]
|
||||
sessionAffinity: {
|
||||
sessionId: {
|
||||
source: 'Cookie'
|
||||
name: 'SessionId'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Merk:** Session awareness bruker Set-Cookie header. Klienten MÅ håndtere cookies korrekt.
|
||||
|
||||
---
|
||||
|
||||
## Health Probe Policies
|
||||
|
||||
### Circuit Breaker som Health Check
|
||||
|
||||
APIM bruker circuit breaker-regler for å vurdere backend-helse, i stedet for tradisjonelle health probes. Når feilbetingelsene i circuit breaker trigges, markeres backend som utilgjengelig og trafikk rutes til neste prioritetsgruppe.
|
||||
|
||||
```bicep
|
||||
resource aoaiBackendWithBreaker 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-westeurope'
|
||||
properties: {
|
||||
url: 'https://aoai-westeurope.openai.azure.com'
|
||||
protocol: 'http'
|
||||
circuitBreaker: {
|
||||
rules: [
|
||||
{
|
||||
failureCondition: {
|
||||
count: 3
|
||||
errorReasons: ['Server errors']
|
||||
interval: 'PT1M'
|
||||
statusCodeRanges: [
|
||||
{ min: 429, max: 429 } // Throttling
|
||||
{ min: 500, max: 599 } // Server errors
|
||||
]
|
||||
}
|
||||
name: 'ai-breaker'
|
||||
tripDuration: 'PT30S'
|
||||
acceptRetryAfter: true // Respekter Retry-After header
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Circuit Breaker Properties
|
||||
|
||||
| Egenskap | Beskrivelse | Anbefalt verdi for AI |
|
||||
|----------|-------------|----------------------|
|
||||
| `count` | Antall feil før trip | 3-5 (avhenger av trafikkmengde) |
|
||||
| `interval` | Tidsvindu for feilmåling | PT1M (1 minutt) |
|
||||
| `statusCodeRanges` | HTTP-koder som telles som feil | 429, 500-599 |
|
||||
| `tripDuration` | Standard varighet for åpen circuit | PT30S - PT5M |
|
||||
| `acceptRetryAfter` | Bruk Retry-After header fra backend | `true` (alltid for Azure OpenAI) |
|
||||
|
||||
### Viktig: Retry-After for Azure OpenAI
|
||||
|
||||
Azure OpenAI returnerer `Retry-After` header ved 429-responser. Verdien kan være stor (opptil 1 dag ved alvorlig overbelastning). Med `acceptRetryAfter: true` respekterer circuit breakeren denne verdien automatisk:
|
||||
|
||||
```
|
||||
Request → 429 (Retry-After: 60) → Circuit åpner → Venter 60 sekunder → Circuit lukker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Health Checks
|
||||
|
||||
### Policy-basert Health Check
|
||||
|
||||
Implementer en custom health endpoint som sjekker backend-tilgjengelighet:
|
||||
|
||||
```xml
|
||||
<!-- Health check API operation -->
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
</inbound>
|
||||
<backend>
|
||||
<!-- Send en minimal request til Azure OpenAI for å sjekke tilgjengelighet -->
|
||||
<forward-request timeout="10" />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
<choose>
|
||||
<when condition="@(context.Response.StatusCode == 200)">
|
||||
<return-response>
|
||||
<set-status code="200" reason="OK" />
|
||||
<set-body>{
|
||||
"status": "healthy",
|
||||
"backend": "@(context.Request.Url.Host)",
|
||||
"region": "@(context.Deployment.Region)",
|
||||
"timestamp": "@(DateTime.UtcNow.ToString("o"))"
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
<otherwise>
|
||||
<return-response>
|
||||
<set-status code="503" reason="Service Unavailable" />
|
||||
<set-body>{
|
||||
"status": "unhealthy",
|
||||
"backend": "@(context.Request.Url.Host)",
|
||||
"statusCode": @(context.Response.StatusCode),
|
||||
"timestamp": "@(DateTime.UtcNow.ToString("o"))"
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</otherwise>
|
||||
</choose>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### APIM Innebygd Health Endpoint
|
||||
|
||||
APIM tilbyr et innebygd status-endepunkt for overvåking:
|
||||
|
||||
```
|
||||
GET https://{apim-name}-{region}-01.regional.azure-api.net/status-0123456789abcdef
|
||||
```
|
||||
|
||||
Bruk dette med Azure Traffic Manager eller egendefinerte overvåkingssystemer.
|
||||
|
||||
---
|
||||
|
||||
## Timeout and Retry Logic
|
||||
|
||||
### Timeout-konfigurasjon for AI-requests
|
||||
|
||||
AI-forespørsler kan ta vesentlig lenger tid enn tradisjonelle API-kall, spesielt for store prompts eller streaming-scenarier:
|
||||
|
||||
| Scenario | Anbefalt Timeout | Begrunnelse |
|
||||
|----------|-----------------|-------------|
|
||||
| Chat completion | 60-120 sekunder | Store prompts, lange responser |
|
||||
| Streaming | 120-240 sekunder | Langt-levende forbindelser |
|
||||
| Embedding | 30-60 sekunder | Typisk raskere enn completions |
|
||||
| Image generation | 120-180 sekunder | DALL-E kan ta lang tid |
|
||||
| Assistants API | 120-300 sekunder | Komplekse tool-kall |
|
||||
|
||||
### Forward-request Policy med Timeout
|
||||
|
||||
```xml
|
||||
<backend>
|
||||
<forward-request timeout="120"
|
||||
fail-on-error-status-code="true"
|
||||
buffer-response="false" />
|
||||
</backend>
|
||||
```
|
||||
|
||||
### Retry Policy for Transiente Feil
|
||||
|
||||
```xml
|
||||
<backend>
|
||||
<retry condition="@(context.Response.StatusCode == 429 ||
|
||||
context.Response.StatusCode >= 500)"
|
||||
count="3"
|
||||
interval="1"
|
||||
delta="2"
|
||||
max-interval="30"
|
||||
first-fast-retry="true">
|
||||
<forward-request timeout="120" buffer-response="false" />
|
||||
</retry>
|
||||
</backend>
|
||||
```
|
||||
|
||||
### Retry vs Circuit Breaker
|
||||
|
||||
| Aspekt | Retry | Circuit Breaker |
|
||||
|--------|-------|----------------|
|
||||
| Scope | Enkelt request | Alle requests til backend |
|
||||
| Formål | Håndtere transiente feil | Beskytte overbelastet backend |
|
||||
| Ventetid | Kort (sekunder) | Lengre (sekunder til minutter) |
|
||||
| Backend-påvirkning | Sender nye requests | Stopper requests |
|
||||
| Kombinasjon | Ja, retry innenfor circuit breaker | Ja, breaker trigger etter retry-feil |
|
||||
|
||||
---
|
||||
|
||||
## Pool Metrics
|
||||
|
||||
### Token-metriker per Backend
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<llm-emit-token-metric namespace="ai-gateway-metrics">
|
||||
<dimension name="Backend" value="@(context.Request.Url.Host)" />
|
||||
<dimension name="Pool" value="@(context.Backend?.Id ?? "direct")" />
|
||||
<dimension name="BackendType" value="@(context.Backend?.Type ?? "unknown")" />
|
||||
<dimension name="Region" value="@(context.Deployment.Region)" />
|
||||
<dimension name="Model" value="@(context.Request.MatchedParameters["deployment-id"])" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### KQL Queries for Pool-overvåking
|
||||
|
||||
**Token-fordeling per backend:**
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(1h)
|
||||
| summarize
|
||||
TotalTokens = sum(TotalTokens),
|
||||
PromptTokens = sum(PromptTokens),
|
||||
CompletionTokens = sum(CompletionTokens),
|
||||
RequestCount = count()
|
||||
by BackendUrl
|
||||
| order by TotalTokens desc
|
||||
```
|
||||
|
||||
**Circuit breaker-trigging per backend:**
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLogs
|
||||
| where TimeGenerated > ago(24h)
|
||||
| where ResponseCode == 503
|
||||
| where BackendResponseCode == 429 or BackendResponseCode >= 500
|
||||
| summarize
|
||||
TripCount = count(),
|
||||
AvgRetryAfter = avg(todouble(ResponseHeaders["Retry-After"]))
|
||||
by BackendUrl, bin(TimeGenerated, 1h)
|
||||
| order by TimeGenerated desc
|
||||
```
|
||||
|
||||
**Backend-tilgjengelighet:**
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLogs
|
||||
| where TimeGenerated > ago(24h)
|
||||
| summarize
|
||||
TotalRequests = count(),
|
||||
SuccessRequests = countif(ResponseCode >= 200 and ResponseCode < 300),
|
||||
ThrottledRequests = countif(ResponseCode == 429),
|
||||
ErrorRequests = countif(ResponseCode >= 500)
|
||||
by BackendUrl, bin(TimeGenerated, 15m)
|
||||
| extend Availability = round(100.0 * SuccessRequests / TotalRequests, 2)
|
||||
| order by TimeGenerated desc
|
||||
```
|
||||
|
||||
### Azure Monitor Alerts for Backend Pools
|
||||
|
||||
```bicep
|
||||
resource backendHealthAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
|
||||
name: 'ai-gateway-backend-errors'
|
||||
location: 'global'
|
||||
properties: {
|
||||
severity: 2
|
||||
evaluationFrequency: 'PT5M'
|
||||
windowSize: 'PT15M'
|
||||
criteria: {
|
||||
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
|
||||
allOf: [
|
||||
{
|
||||
name: 'HighBackendErrors'
|
||||
metricName: 'BackendRequestCount'
|
||||
operator: 'GreaterThan'
|
||||
threshold: 50
|
||||
timeAggregation: 'Total'
|
||||
dimensions: [
|
||||
{
|
||||
name: 'BackendResponseCodeCategory'
|
||||
operator: 'Include'
|
||||
values: ['5xx']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
actions: [
|
||||
{
|
||||
actionGroupId: actionGroup.id
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Backend Pool Design for AI
|
||||
|
||||
| Anbefaling | Begrunnelse |
|
||||
|------------|-------------|
|
||||
| PTU som Priority 1, Standard som Priority 2 | Utnytter fast PTU-kapasitet først |
|
||||
| Circuit breaker med `acceptRetryAfter: true` | Respekterer Azure OpenAI throttling |
|
||||
| Separate pools per region | Unngår cross-region latens |
|
||||
| Session awareness for chat | Sikrer kontekst i samtaler |
|
||||
| Maks 30 backends per pool | APIM-begrensning |
|
||||
|
||||
### Anti-patterns
|
||||
|
||||
| Anti-pattern | Problem | Løsning |
|
||||
|-------------|---------|---------|
|
||||
| Én pool med backends i mange regioner | Cross-region latens | Regionspesifikke pools |
|
||||
| Ingen circuit breaker | Backend overbelastes | Alltid konfigurer circuit breaker |
|
||||
| Kort tripDuration | Konstant flapping | Minimum 30 sekunder |
|
||||
| Retry uten circuit breaker | DDoS mot egen backend | Kombiner begge |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Backends in API Management](https://learn.microsoft.com/en-us/azure/api-management/backends) — Offisiell backend-dokumentasjon
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) — AI gateway resiliency-funksjoner
|
||||
- [Circuit breaker pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker) — Arkitekturmønster for circuit breaker
|
||||
- [Smart Load Balancing for OpenAI Endpoints](https://github.com/Azure-Samples/openai-apim-lb) — GitHub sample med priority-basert routing
|
||||
- [GenAI Gateway Toolkit](https://github.com/Azure-Samples/ai-gateway) — Lab-exercises for AI gateway
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunder trenger å sette opp load balancing mellom flere Azure OpenAI-instanser via APIM, spesielt for PTU + Standard fallback-mønstre.
|
||||
- Anbefal alltid circuit breaker med `acceptRetryAfter: true` for Azure OpenAI backends — dette er kritisk for å håndtere 429 throttling uten å overbelaste backend.
|
||||
- Husk begrensningen: Maks 30 backends per pool. For store organisasjoner med mange regioner, bruk hierarkiske pools (en pool per region, med regional routing via policy).
|
||||
- Session awareness er viktig for Assistants API og chat-scenarier — uten dette kan tråder miste kontekst når requests rutes til forskjellige backends.
|
||||
- Circuit breaker-regler er tilnærmede (approximate) pga. distribuert arkitektur — ulike gateway-instanser synkroniserer ikke circuit state.
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
# Caching Strategies for AI Responses in APIM
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Caching er en av de mest effektive strategiene for a redusere kostnader og forbedre ytelse i AI-applikasjoner. Azure API Management tilbyr bade tradisjonell HTTP-caching og semantisk caching spesielt designet for LLM-API-er. Semantisk caching bruker embedding-vektorer for a identifisere prompts som er semantisk like -- ikke bare identiske -- og returnere cachede svar uten a kalle backend-modellen.
|
||||
|
||||
For norsk offentlig sektor kan caching-strategier gi vesentlige besparelser. En typisk offentlig virksomhet som bruker Azure OpenAI for chatbot-tjenester, intern dokumentanalyse eller innbyggerveiledning vil ofte motta mange lignende sporsmol. Semantisk caching kan redusere token-forbruket med 20-40% for slike workloads, med tilsvarende kostnadsbesparelse og forbedret responstid.
|
||||
|
||||
APIM stotter to hovedtyper caching: intern (innebygd) og ekstern (Redis-basert). For semantisk caching av AI-svar er ekstern cache via Azure Managed Redis med RediSearch-modulen pakrevd. Denne referansen dekker bade tradisjonell og semantisk caching, med fokus pa praktisk implementering for AI-workloads.
|
||||
|
||||
---
|
||||
|
||||
## Prompt-baserte caching-nokler
|
||||
|
||||
### Tradisjonell caching med eksakte matcher
|
||||
|
||||
For identiske prompts kan standard `cache-lookup` / `cache-store` policies brukes:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Cache lookup based on exact request body hash -->
|
||||
<cache-lookup vary-by-developer="false" vary-by-developer-groups="false">
|
||||
<vary-by-header>x-tenant-id</vary-by-header>
|
||||
<vary-by-query-parameter>model</vary-by-query-parameter>
|
||||
</cache-lookup>
|
||||
</inbound>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Store response in cache for 5 minutes -->
|
||||
<cache-store duration="300" />
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Custom cache-nokler for AI-foresporsler
|
||||
|
||||
Bygg tilpassede cache-nokler basert pa prompt-innhold:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Generate cache key from normalized prompt content -->
|
||||
<set-variable name="cacheKey" value="@{
|
||||
var body = context.Request.Body.As<JObject>(preserveContent: true);
|
||||
var messages = (JArray)body?["messages"];
|
||||
if (messages == null) return "";
|
||||
|
||||
// Build key from role+content pairs, normalized
|
||||
var keyParts = new System.Collections.Generic.List<string>();
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
var role = msg["role"]?.ToString() ?? "";
|
||||
var content = msg["content"]?.ToString()?.Trim().ToLower() ?? "";
|
||||
keyParts.Add($"{role}:{content}");
|
||||
}
|
||||
|
||||
var model = body["model"]?.ToString() ?? "default";
|
||||
var combined = model + "|" + string.Join("|", keyParts);
|
||||
|
||||
// Generate SHA256 hash
|
||||
using (var sha = System.Security.Cryptography.SHA256.Create())
|
||||
{
|
||||
var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combined));
|
||||
return BitConverter.ToString(bytes).Replace("-", "").ToLower();
|
||||
}
|
||||
}" />
|
||||
|
||||
<!-- Lookup in cache -->
|
||||
<cache-lookup-value key="@((string)context.Variables["cacheKey"])"
|
||||
variable-name="cachedResponse" />
|
||||
|
||||
<choose>
|
||||
<when condition="@(context.Variables.ContainsKey("cachedResponse"))">
|
||||
<return-response>
|
||||
<set-status code="200" reason="OK" />
|
||||
<set-header name="Content-Type" exists-action="override">
|
||||
<value>application/json</value>
|
||||
</set-header>
|
||||
<set-header name="x-cache-hit" exists-action="override">
|
||||
<value>true</value>
|
||||
</set-header>
|
||||
<set-body>@((string)context.Variables["cachedResponse"])</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Store successful responses in cache -->
|
||||
<choose>
|
||||
<when condition="@(context.Response.StatusCode == 200)">
|
||||
<cache-store-value key="@((string)context.Variables["cacheKey"])"
|
||||
value="@(context.Response.Body.As<string>(preserveContent: true))"
|
||||
duration="600" />
|
||||
<set-header name="x-cache-hit" exists-action="override">
|
||||
<value>false</value>
|
||||
</set-header>
|
||||
</when>
|
||||
</choose>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Semantisk deduplisering
|
||||
|
||||
### Oversikt over semantisk caching i APIM
|
||||
|
||||
Semantisk caching bruker embeddings for a matche prompts basert pa meningsbetydning, ikke bare eksakt tekst. To prompts som "Hva er Microsoft Azure?" og "Kan du forklare hva Azure er?" vil ga i cache-treff selv om ordlyden er ulik.
|
||||
|
||||
### Arkitektur
|
||||
|
||||
```
|
||||
Klient --> APIM --> [Semantic Cache Lookup] --> Azure Managed Redis (RediSearch)
|
||||
| |
|
||||
| (cache miss) | (cache hit)
|
||||
v v
|
||||
[Embeddings API] [Returner cached svar]
|
||||
|
|
||||
v
|
||||
[AI Backend (Chat)]
|
||||
|
|
||||
v
|
||||
[Semantic Cache Store] --> Azure Managed Redis
|
||||
```
|
||||
|
||||
### Forutsetninger
|
||||
|
||||
| Komponent | Krav |
|
||||
|-----------|------|
|
||||
| Azure Managed Redis | RediSearch-modul aktivert (velges ved opprettelse) |
|
||||
| Embeddings deployment | text-embedding-ada-002 eller nyere modell |
|
||||
| APIM | Alle tiers stotter semantisk caching med ekstern cache |
|
||||
| Autentisering | Managed Identity til bade OpenAI og Redis |
|
||||
|
||||
### Konfigurering av semantisk caching
|
||||
|
||||
#### 1. Opprett embeddings-backend
|
||||
|
||||
```xml
|
||||
<!-- Backend for embeddings API -->
|
||||
<set-backend-service backend-id="embeddings-backend" />
|
||||
```
|
||||
|
||||
I Azure Portal:
|
||||
- **Type:** Custom URL
|
||||
- **Runtime URL:** `https://{aoai-name}.openai.azure.com/openai/deployments/{embedding-deployment}/embeddings`
|
||||
- **Managed Identity:** System-assigned, Resource ID: `https://cognitiveservices.azure.com/`
|
||||
|
||||
#### 2. Konfigurer semantic cache lookup (inbound)
|
||||
|
||||
For Azure OpenAI API-er:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Semantic cache lookup -->
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
ignore-system-messages="true"
|
||||
max-message-count="10">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
|
||||
<!-- Rate limit as fallback if cache is unavailable -->
|
||||
<rate-limit calls="20" renewal-period="60" />
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
For andre LLM-API-er (ikke Azure OpenAI):
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<llm-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
ignore-system-messages="true"
|
||||
max-message-count="10">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</llm-semantic-cache-lookup>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
#### 3. Konfigurer semantic cache store (outbound)
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Store response for 60 seconds -->
|
||||
<azure-openai-semantic-cache-store duration="60" />
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TTL-konfigurasjon
|
||||
|
||||
### Strategier for Time-to-Live
|
||||
|
||||
Riktig TTL-konfigurasjon balanserer mellom kostnadsbesparelse og datakvalitet:
|
||||
|
||||
| Bruksscenario | Anbefalt TTL | Begrunnelse |
|
||||
|--------------|-------------|-------------|
|
||||
| FAQ/statisk veiledning | 3600s (1 time) | Innholdet endres sjelden |
|
||||
| Generell chatbot | 300s (5 min) | Balanse mellom friskhet og kostnad |
|
||||
| Dokumentanalyse | 600s (10 min) | Dokumenter endres sjelden innen sesjon |
|
||||
| Sanntidsdata-sporring | 30-60s | Data kan endres raskt |
|
||||
| Kodegenerering | 120s (2 min) | Brukere itererer raskt |
|
||||
| Intern kunnskapssok | 1800s (30 min) | Intern kunnskap er relativt stabil |
|
||||
|
||||
### Dynamisk TTL basert pa kontekst
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Dynamic TTL based on request type -->
|
||||
<set-variable name="cacheDuration" value="@{
|
||||
var body = context.Request.Body.As<JObject>(preserveContent: true);
|
||||
var messages = (JArray)body?["messages"];
|
||||
var lastMessage = messages?.Last?["content"]?.ToString() ?? "";
|
||||
|
||||
// Longer TTL for FAQ-like questions
|
||||
if (lastMessage.Contains("hva er") || lastMessage.Contains("forklar"))
|
||||
return 3600;
|
||||
|
||||
// Shorter TTL for data queries
|
||||
if (lastMessage.Contains("status") || lastMessage.Contains("siste"))
|
||||
return 60;
|
||||
|
||||
// Default TTL
|
||||
return 300;
|
||||
}" />
|
||||
|
||||
<azure-openai-semantic-cache-store
|
||||
duration="@((int)context.Variables["cacheDuration"])" />
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache-invalidering
|
||||
|
||||
### Manuell invalidering med cache-remove-value
|
||||
|
||||
```xml
|
||||
<!-- Remove specific cached value -->
|
||||
<cache-remove-value key="specific-cache-key" />
|
||||
```
|
||||
|
||||
### Automatisk invalidering ved modellbytte
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Include model version in cache key to auto-invalidate on model change -->
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
ignore-system-messages="true"
|
||||
max-message-count="10">
|
||||
<!-- Vary by model deployment to invalidate cache on model update -->
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
<vary-by>@(context.Request.Headers.GetValueOrDefault("x-model-version", "default"))</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Cache-invalidering via API-kall
|
||||
|
||||
Opprett en dedikert operasjon for administratorer:
|
||||
|
||||
```xml
|
||||
<!-- Cache purge operation -->
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Verify admin access -->
|
||||
<validate-jwt header-name="Authorization"
|
||||
failed-validation-httpcode="401"
|
||||
failed-validation-error-message="Unauthorized">
|
||||
<required-claims>
|
||||
<claim name="roles" match="any">
|
||||
<value>CacheAdmin</value>
|
||||
</claim>
|
||||
</required-claims>
|
||||
</validate-jwt>
|
||||
|
||||
<!-- Purge cache - requires external cache API call -->
|
||||
<send-request mode="new" response-variable-name="purgeResult" timeout="10">
|
||||
<set-url>@($"https://{cacheHost}:10000/FLUSHDB")</set-url>
|
||||
<set-method>POST</set-method>
|
||||
</send-request>
|
||||
|
||||
<return-response>
|
||||
<set-status code="200" reason="Cache Purged" />
|
||||
<set-body>{"status":"cache_purged","timestamp":"@(DateTime.UtcNow.ToString("o"))"}</set-body>
|
||||
</return-response>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kostnadsbesparelsesanalyse
|
||||
|
||||
### Beregningsmodell
|
||||
|
||||
| Parameter | Verdi |
|
||||
|-----------|-------|
|
||||
| Gjennomsnittlig tokens per request | 2 000 (prompt) + 500 (completion) |
|
||||
| GPT-4o pris per 1M input tokens | $2.50 |
|
||||
| GPT-4o pris per 1M output tokens | $10.00 |
|
||||
| Antall requests per dag | 10 000 |
|
||||
| Gjennomsnittlig cache hit rate | 30% |
|
||||
|
||||
### Kostnadsberegning
|
||||
|
||||
| Scenario | Daglig kostnad (NOK) | Manedlig kostnad (NOK) |
|
||||
|----------|---------------------|----------------------|
|
||||
| Uten caching | ~750 | ~22 500 |
|
||||
| Med 30% cache hit | ~525 | ~15 750 |
|
||||
| Med 50% cache hit | ~375 | ~11 250 |
|
||||
| Med 70% cache hit | ~225 | ~6 750 |
|
||||
|
||||
### Tilleggskostnader for caching-infrastruktur
|
||||
|
||||
| Komponent | Manedlig kostnad (NOK) |
|
||||
|-----------|----------------------|
|
||||
| Azure Managed Redis (Balanced B1) | ~2 500 |
|
||||
| Embeddings API-kall (for semantisk caching) | ~150 |
|
||||
| **Total caching-overhead** | **~2 650** |
|
||||
|
||||
### Netto besparelse ved 30% hit rate
|
||||
|
||||
- Besparelse: 22 500 - 15 750 = **6 750 NOK/mnd**
|
||||
- Caching-kostnad: **2 650 NOK/mnd**
|
||||
- **Netto besparelse: ~4 100 NOK/mnd** (18% av total)
|
||||
|
||||
### Score-threshold tuning
|
||||
|
||||
`score-threshold` i semantisk caching pavirker hit rate og kvalitet:
|
||||
|
||||
| Threshold | Hit Rate | Kvalitetsrisiko |
|
||||
|-----------|----------|----------------|
|
||||
| 0.05 | Hoy (50-70%) | Hoy -- kan returnere irrelevante svar |
|
||||
| 0.10 | Middels-hoy (30-50%) | Lav-middels |
|
||||
| 0.15 (anbefalt) | Middels (20-35%) | Lav |
|
||||
| 0.25 | Lav (10-15%) | Svart lav |
|
||||
| 0.50 | Svart lav (<5%) | Neglisjerbar |
|
||||
|
||||
---
|
||||
|
||||
## Caching-tjenester: Intern vs. Ekstern
|
||||
|
||||
| Egenskap | Intern cache | Ekstern (Redis) |
|
||||
|----------|-------------|----------------|
|
||||
| Automatisk provisjonering | Ja | Nei |
|
||||
| Tilleggskostnad | Nei | Ja |
|
||||
| Semantisk caching | Nei | Ja |
|
||||
| Tilgjengelig i alle tiers | Nei (ikke Consumption) | Ja |
|
||||
| Persistent lagring | Ja (v2), Nei (classic) | Ja |
|
||||
| Delt mellom instanser | Nei | Ja |
|
||||
| Data preloading | Nei | Ja |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Caching overview in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/caching-overview) -- oversikt over caching-alternativer
|
||||
- [Enable semantic caching for LLM APIs](https://learn.microsoft.com/en-us/azure/api-management/azure-openai-enable-semantic-caching) -- trinnvis veiledning
|
||||
- [AI gateway capabilities - Semantic caching](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities#scalability-and-performance) -- AI gateway-kontekst
|
||||
- [llm-semantic-cache-lookup policy](https://learn.microsoft.com/en-us/azure/api-management/llm-semantic-cache-lookup-policy) -- policy-referanse
|
||||
- [llm-semantic-cache-store policy](https://learn.microsoft.com/en-us/azure/api-management/llm-semantic-cache-store-policy) -- policy-referanse
|
||||
- [Set up an external cache in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-cache-external) -- Redis-oppsett
|
||||
- [Application design for AI workloads - Caching strategies](https://learn.microsoft.com/en-us/azure/well-architected/ai/application-design#implement-multi-layer-caching-strategies) -- Well-Architected-anbefalinger
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** nar kunden onsker a redusere AI-kostnader gjennom caching, eller nar de trenger a forbedre responstider for brukere som stiller lignende sporsmol.
|
||||
- Start med `score-threshold="0.15"` for semantisk caching -- dette gir god balanse. Juster ned til 0.10 for hoyere hit rate i FAQ-scenarier, eller opp til 0.25 for mer presise matcher i kritiske applikasjoner.
|
||||
- Husk at semantisk caching krever Azure Managed Redis med RediSearch-modulen -- denne modulen ma velges ved opprettelse av Redis-instansen og kan ikke legges til i ettertid.
|
||||
- For norsk offentlig sektor med hoy grad av repetitive sporsmol (innbyggertjenester, veiledning), er semantisk caching en lavthengende frukt med typisk 20-40% kostnadsreduksjon.
|
||||
- Inkluder alltid `<vary-by>@(context.Subscription.Id)</vary-by>` for a forhindre at en leietakers svar returneres til en annen -- dette er kritisk for personvern og dataskille.
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
# Circuit Breaker Patterns for AI Models
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Circuit breaker-mønsteret er en grunnleggende resiliensmekanisme for AI-applikasjoner som kommuniserer med Azure OpenAI og andre LLM-backends. Når en backend-tjeneste blir overbelastet eller utilgjengelig, forhindrer circuit breaker at applikasjonen fortsetter å sende forespørsler som uansett vil feile. I stedet "bryter kretsen" og returnerer en feilmelding umiddelbart, slik at backend-tjenesten får tid til å gjenopprette seg.
|
||||
|
||||
For Azure AI-tjenester er circuit breaker spesielt viktig fordi Azure OpenAI returnerer 429 (Too Many Requests) med en `Retry-After`-header som kan ha verdier opp til 24 timer. Uten circuit breaker vil applikasjoner fortsette å hamre på en throttlet tjeneste, noe som forverrer situasjonen og kaster bort gateway-ressurser. APIMsintegrerte circuit breaker håndterer dette automatisk ved å respektere `Retry-After`-headeren.
|
||||
|
||||
I kombinasjon med backend pools og lastbalansering utgjør circuit breaker selve nervesystemet i en intelligent AI-gateway: den detekterer problemer, isolerer feilende backends, ruter trafikk til friske alternativer, og gjenoppretter normal drift automatisk når backend er tilbake.
|
||||
|
||||
---
|
||||
|
||||
## Circuit Breaker State Machine
|
||||
|
||||
### Tre tilstander
|
||||
|
||||
Circuit breaker opererer som en tilstandsmaskin med tre tilstander:
|
||||
|
||||
```
|
||||
Feil > threshold
|
||||
┌─────────┐ ───────────────► ┌──────────┐
|
||||
│ CLOSED │ │ OPEN │
|
||||
│ (normal)│ ◄────────────── │ (trip) │
|
||||
└─────────┘ Alle OK i └──────────┘
|
||||
│ half-open │
|
||||
│ │ Trip duration
|
||||
│ │ utløpt
|
||||
│ ┌──────────────┐ │
|
||||
│ │ HALF-OPEN │◄───┘
|
||||
│ │ (test) │
|
||||
│ └──────────────┘
|
||||
│ │
|
||||
│ Suksess │ Feil
|
||||
◄──────────────┘───────────► OPEN
|
||||
```
|
||||
|
||||
| Tilstand | Oppførsel | Varighet |
|
||||
|----------|-----------|----------|
|
||||
| **Closed** | Normal drift, alle requests videresendes til backend | Ubegrenset (til feilbetingelse oppstår) |
|
||||
| **Open** | Alle requests avvises umiddelbart med 503 | Trip duration (konfigurerbar, eller fra Retry-After) |
|
||||
| **Half-Open** | Et begrenset antall test-requests sendes til backend | Til nok suksesser ELLER ny feil |
|
||||
|
||||
### APIM-spesifikk oppførsel
|
||||
|
||||
APIMcircuit breaker har noen viktige forskjeller fra den generelle circuit breaker-mønsteret:
|
||||
|
||||
| Egenskap | APIM Circuit Breaker |
|
||||
|----------|---------------------|
|
||||
| **Konfigurasjons-scope** | Per backend (ikke per API eller policy) |
|
||||
| **Antall regler** | Kun én regel per backend (p.t.) |
|
||||
| **Synkronisering** | Ingen mellom gateway-instanser (approksimasjon) |
|
||||
| **Retry-After respekt** | Ja, dynamisk trip duration basert på backend-respons |
|
||||
| **Støttede tiers** | Alle unntatt Consumption |
|
||||
| **Respons ved Open** | 503 Service Unavailable |
|
||||
|
||||
---
|
||||
|
||||
## Konfigurasjon
|
||||
|
||||
### Grunnleggende circuit breaker for Azure OpenAI
|
||||
|
||||
```bicep
|
||||
resource openaiBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-norwayeast'
|
||||
properties: {
|
||||
url: 'https://aoai-norwayeast.openai.azure.com/openai'
|
||||
protocol: 'http'
|
||||
circuitBreaker: {
|
||||
rules: [
|
||||
{
|
||||
name: 'ai-resilience-rule'
|
||||
failureCondition: {
|
||||
count: 3
|
||||
interval: 'PT1M'
|
||||
statusCodeRanges: [
|
||||
{ min: 429, max: 429 }
|
||||
{ min: 500, max: 599 }
|
||||
]
|
||||
}
|
||||
tripDuration: 'PT10S'
|
||||
acceptRetryAfter: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Attributter forklart
|
||||
|
||||
| Attributt | Type | Beskrivelse | Anbefalt verdi for AI |
|
||||
|-----------|------|-------------|----------------------|
|
||||
| `failureCondition.count` | int | Antall feil som trigger trip | 3-5 |
|
||||
| `failureCondition.interval` | duration | Tidsvindu for feil-telling | PT1M (1 minutt) |
|
||||
| `failureCondition.statusCodeRanges` | array | HTTP-koder som teller som feil | 429 + 500-599 |
|
||||
| `tripDuration` | duration | Hvor lenge circuit er åpent | PT10S - PT1M |
|
||||
| `acceptRetryAfter` | bool | Bruk Retry-After header som trip duration | **true** (alltid for AI) |
|
||||
|
||||
### Prosentbasert vs. count-basert triggering
|
||||
|
||||
APIM støtter to modeller for feil-deteksjon:
|
||||
|
||||
**Count-basert (anbefalt for AI):**
|
||||
```json
|
||||
{
|
||||
"failureCondition": {
|
||||
"count": 3,
|
||||
"interval": "PT1M",
|
||||
"statusCodeRanges": [{"min": 429, "max": 429}]
|
||||
}
|
||||
}
|
||||
```
|
||||
Trigger: 3 eller flere 429-responser innen 1 minutt.
|
||||
|
||||
**Prosentbasert:**
|
||||
```json
|
||||
{
|
||||
"failureCondition": {
|
||||
"percentage": 50,
|
||||
"interval": "PT1M",
|
||||
"statusCodeRanges": [{"min": 429, "max": 429}]
|
||||
}
|
||||
}
|
||||
```
|
||||
Trigger: 50% eller mer av requests feiler innen 1 minutt.
|
||||
|
||||
**Anbefaling:** Count-basert er mer forutsigbar for AI-workloads. Prosentbasert kan gi falske positiver ved lav trafikk (2 av 3 requests feiler = 66%).
|
||||
|
||||
---
|
||||
|
||||
## Failure Threshold Tuning
|
||||
|
||||
### Faktorene som påvirker threshold
|
||||
|
||||
| Faktor | Lav threshold (1-2) | Høy threshold (5-10) |
|
||||
|--------|---------------------|---------------------|
|
||||
| **Reaksjonstid** | Rask, reagerer umiddelbart | Tregere, tolererer noen feil |
|
||||
| **Falske positiver** | Høy risiko | Lav risiko |
|
||||
| **Backend-beskyttelse** | Sterk, minimal ekstra belastning | Svakere, flere feil-requests |
|
||||
| **Anbefalt for** | Kritiske, kapasitetsbegrensede backends | Robuste backends med transiente feil |
|
||||
|
||||
### Anbefalte innstillinger per scenario
|
||||
|
||||
| Scenario | count | interval | tripDuration | acceptRetryAfter |
|
||||
|----------|-------|----------|--------------|-----------------|
|
||||
| **PTU-instans (high priority)** | 3 | PT1M | PT10S | true |
|
||||
| **PAYGO-instans (fallback)** | 5 | PT2M | PT30S | true |
|
||||
| **Dev/test** | 2 | PT30S | PT5S | true |
|
||||
| **Business-critical** | 3 | PT1M | PT10S | true |
|
||||
| **Batch processing** | 10 | PT5M | PT1M | true |
|
||||
|
||||
### Dynamisk trip duration med Retry-After
|
||||
|
||||
Når `acceptRetryAfter: true` er satt, overstyrer Azure OpenAIs `Retry-After`-header den konfigurerte `tripDuration`:
|
||||
|
||||
```
|
||||
Scenario: OpenAI returnerer 429 med Retry-After: 30
|
||||
→ Circuit breaker åpner i 30 sekunder (ikke konfiguert tripDuration)
|
||||
→ Etter 30 sekunder: half-open → test request
|
||||
→ Suksess: circuit lukkes
|
||||
→ Feil: circuit åpner igjen med ny Retry-After
|
||||
|
||||
Scenario: OpenAI returnerer 429 med Retry-After: 86400 (1 dag!)
|
||||
→ Circuit breaker åpner i 24 timer
|
||||
→ All trafikk rutes til andre backends i poolen
|
||||
```
|
||||
|
||||
> **Viktig advarsel:** Azure OpenAI kan returnere Retry-After verdier opp til 1 dag (86400 sekunder). Med `acceptRetryAfter: true` vil dette bety at backend er utilgjengelig i 24 timer. Sørg for at backend-poolen har nok kapasitet i andre backends til å håndtere dette.
|
||||
|
||||
---
|
||||
|
||||
## Fallback-policies
|
||||
|
||||
### Mønster 1: Backend Pool med automatisk failover
|
||||
|
||||
Den enkleste og mest robuste tilnærmingen:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- Backend pool håndterer failover automatisk -->
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com/" />
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
Når circuit breaker utløses på Priority 1-backends, ruter APIM automatisk til Priority 2, osv.
|
||||
|
||||
### Mønster 2: Retry med backend-bytte
|
||||
|
||||
Eksplisitt retry-logikk som bytter backend ved 429:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<backend>
|
||||
<retry condition="@(context.Response.StatusCode == 429)"
|
||||
count="3"
|
||||
interval="0"
|
||||
first-fast-retry="true">
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
</retry>
|
||||
</backend>
|
||||
</policies>
|
||||
```
|
||||
|
||||
**Viktig:** `interval="0"` betyr umiddelbar retry til neste backend, IKKE ventetid. Server-side retries bør aldri ha delay — det holder opp klienten og bruker gateway-ressurser.
|
||||
|
||||
### Mønster 3: Graceful Degradation med cache-fallback
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- Prøv semantic cache først -->
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.20"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned" />
|
||||
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
</inbound>
|
||||
|
||||
<on-error>
|
||||
<choose>
|
||||
<!-- Alle backends utilgjengelige -->
|
||||
<when condition="@(context.Response.StatusCode == 503)">
|
||||
<return-response>
|
||||
<set-status code="503" reason="AI Service Temporarily Unavailable" />
|
||||
<set-header name="Retry-After" exists-action="override">
|
||||
<value>60</value>
|
||||
</set-header>
|
||||
<set-body>@{
|
||||
return new JObject(
|
||||
new JProperty("error", new JObject(
|
||||
new JProperty("code", "service_unavailable"),
|
||||
new JProperty("message", "AI-tjenesten er midlertidig utilgjengelig. Prøv igjen om 60 sekunder."),
|
||||
new JProperty("type", "circuit_breaker_open")
|
||||
))
|
||||
).ToString();
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Mønster 4: Fallback til enklere modell
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<backend>
|
||||
<retry condition="@(context.Response.StatusCode == 429 || context.Response.StatusCode == 503)"
|
||||
count="1"
|
||||
interval="0"
|
||||
first-fast-retry="true">
|
||||
<!-- Fallback til mindre modell -->
|
||||
<set-backend-service backend-id="openai-gpt4o-mini-backend" />
|
||||
<rewrite-uri template="/deployments/gpt-4o-mini/chat/completions" />
|
||||
</retry>
|
||||
</backend>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recovery-mekanismer
|
||||
|
||||
### Automatisk recovery
|
||||
|
||||
Circuit breaker håndterer recovery automatisk:
|
||||
|
||||
```
|
||||
1. Trip: Circuit åpner (503 til klienter)
|
||||
2. Wait: tripDuration utløper (eller Retry-After)
|
||||
3. Half-Open: Test-request sendes til backend
|
||||
4. Success: Circuit lukkes, normal drift gjenopptas
|
||||
5. Failure: Circuit åpner igjen, ny tripDuration starter
|
||||
```
|
||||
|
||||
### Recovery-timing
|
||||
|
||||
| Kilde | Prioritet | Typisk verdi |
|
||||
|-------|-----------|--------------|
|
||||
| `Retry-After` header (når `acceptRetryAfter: true`) | Høyest | 1-86400 sekunder |
|
||||
| Konfigurert `tripDuration` | Fallback | PT10S - PT1M |
|
||||
| Default (uten konfigurasjon) | Lavest | PT1M |
|
||||
|
||||
### Overvåking av circuit breaker-tilstand
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<!-- Log circuit breaker state -->
|
||||
<trace source="circuit-breaker" severity="information">
|
||||
<message>@{
|
||||
var backendId = context.Backend?.Id ?? "unknown";
|
||||
var statusCode = context.Response.StatusCode;
|
||||
return $"Backend: {backendId}, Status: {statusCode}";
|
||||
}</message>
|
||||
</trace>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
**KQL for circuit breaker-hendelser:**
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLogs
|
||||
| where ResponseCode == 503
|
||||
| extend backendId = tostring(parse_json(BackendResponseBody).backendId)
|
||||
| summarize CircuitBreakerTrips = count() by backendId, bin(TimeGenerated, 5m)
|
||||
| render timechart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timeout-konfigurasjon
|
||||
|
||||
### Forward-request timeout
|
||||
|
||||
Kontroller hvor lenge APIM venter på backend-respons:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<backend>
|
||||
<forward-request timeout="120" />
|
||||
</backend>
|
||||
</policies>
|
||||
```
|
||||
|
||||
**Anbefalte timeouts for AI-workloads:**
|
||||
|
||||
| Operasjon | Anbefalt timeout | Begrunnelse |
|
||||
|-----------|------------------|-------------|
|
||||
| Chat Completion (standard) | 60-120 sek | GPT-4o kan bruke tid på komplekse prompts |
|
||||
| Chat Completion (streaming) | 120-180 sek | Streaming starter raskt, men kan vare lenge |
|
||||
| Embeddings | 30-60 sek | Raskere operasjon, typisk < 10 sek |
|
||||
| Image Generation (DALL-E) | 120-180 sek | Bildegenerering er CPU-intensivt |
|
||||
| Assistants API | 180-300 sek | Multi-step agent workflows |
|
||||
| Batch API | 300-600 sek | Store batch-operasjoner |
|
||||
|
||||
### Timeout + Circuit Breaker interaksjon
|
||||
|
||||
```
|
||||
Timeout utløper (120 sek)
|
||||
→ APIM registrerer det som feil
|
||||
→ Teller mot circuit breaker threshold
|
||||
→ Etter N timeouts → Circuit breaker trip
|
||||
→ Backend fjernes fra pool → Trafikk til alternativer
|
||||
```
|
||||
|
||||
> **Best practice:** Sett timeout lavere enn det du tror er nødvendig. Bedre å få rask feil og retry til annen backend enn å vente 120 sekunder på en hengende request.
|
||||
|
||||
---
|
||||
|
||||
## Avanserte mønstre
|
||||
|
||||
### Mønster: Cascading Circuit Breakers
|
||||
|
||||
For multi-tier arkitekturer der APIM ruter til mellomtjenester som igjen kaller Azure OpenAI:
|
||||
|
||||
```
|
||||
Client → APIM [CB-1] → Custom Service [CB-2] → Azure OpenAI
|
||||
```
|
||||
|
||||
```bicep
|
||||
// CB-1: APIM → Custom Service
|
||||
resource customServiceBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
properties: {
|
||||
circuitBreaker: {
|
||||
rules: [{
|
||||
failureCondition: {
|
||||
count: 5
|
||||
interval: 'PT2M'
|
||||
statusCodeRanges: [
|
||||
{ min: 502, max: 504 }
|
||||
]
|
||||
}
|
||||
tripDuration: 'PT30S'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Applikasjons-nivå CB-2 implementeres med Polly (.NET), resilience4j (Java), eller tilsvarende.
|
||||
|
||||
### Mønster: Health Endpoint Monitoring
|
||||
|
||||
Kombiner circuit breaker med aktiv health checking:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- Sjekk health endpoint før ruting -->
|
||||
<send-request mode="new"
|
||||
response-variable-name="healthCheck"
|
||||
timeout="5"
|
||||
ignore-error="true">
|
||||
<set-url>@("https://aoai-norwayeast.openai.azure.com/openai/models?api-version=2024-10-21")</set-url>
|
||||
<set-method>GET</set-method>
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com/" />
|
||||
</send-request>
|
||||
|
||||
<choose>
|
||||
<when condition="@(((IResponse)context.Variables["healthCheck"]).StatusCode != 200)">
|
||||
<!-- Backend er nede, bruk fallback direkte -->
|
||||
<set-backend-service backend-id="openai-fallback-pool" />
|
||||
</when>
|
||||
<otherwise>
|
||||
<set-backend-service backend-id="openai-primary-pool" />
|
||||
</otherwise>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
> **Merk:** Health endpoint monitoring legger til latens og backend-belastning. Bruk det kun for scenarier der circuit breaker alene ikke gir rask nok failover.
|
||||
|
||||
---
|
||||
|
||||
## Anti-mønstre
|
||||
|
||||
| Anti-mønster | Problem | Løsning |
|
||||
|--------------|---------|---------|
|
||||
| **Ingen circuit breaker** | Backend overbelastes, cascading failure | Aktiver circuit breaker på alle AI-backends |
|
||||
| **For lav threshold (count=1)** | Falske positiver ved transiente feil | Bruk count=3-5 for produksjon |
|
||||
| **For lang tripDuration** | Backend er unødvendig utilgjengelig | Bruk `acceptRetryAfter: true` |
|
||||
| **Ignorere Retry-After** | Hammer backend som eksplisitt ber om pause | Sett `acceptRetryAfter: true` alltid |
|
||||
| **Server-side delay** | Retry med sleep/delay holder opp klienter | Bruk `interval="0"` med backend pool failover |
|
||||
| **Timeout for lang** | Gateway-ressurser brukt opp på hengende requests | Sett realistiske timeouts per operasjonstype |
|
||||
| **Mangle monitoring** | Ingen innsikt i circuit breaker-oppførsel | Emit metrikk + KQL dashboards |
|
||||
|
||||
---
|
||||
|
||||
## Komplett resiliens-policy
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- 1. Token rate limiting -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
estimate-prompt-tokens="true" />
|
||||
|
||||
<!-- 2. Backend pool med circuit breaker -->
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
|
||||
<!-- 3. Managed Identity auth -->
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com/" />
|
||||
</inbound>
|
||||
|
||||
<backend>
|
||||
<!-- 4. Retry til annen backend ved 429 -->
|
||||
<retry condition="@(context.Response.StatusCode == 429)"
|
||||
count="3"
|
||||
interval="0"
|
||||
first-fast-retry="true">
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
</retry>
|
||||
|
||||
<!-- 5. Timeout -->
|
||||
<forward-request timeout="120" />
|
||||
</backend>
|
||||
|
||||
<outbound>
|
||||
<!-- 6. Token metrikk -->
|
||||
<llm-emit-token-metric namespace="ai-gateway">
|
||||
<dimension name="Backend" value="@(context.Backend?.Id ?? "unknown")" />
|
||||
<dimension name="StatusCode" value="@(context.Response.StatusCode.ToString())" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
|
||||
<on-error>
|
||||
<!-- 7. Graceful degradation -->
|
||||
<choose>
|
||||
<when condition="@(context.Response.StatusCode == 503)">
|
||||
<return-response>
|
||||
<set-status code="503" reason="AI Service Unavailable" />
|
||||
<set-header name="Retry-After" exists-action="override">
|
||||
<value>60</value>
|
||||
</set-header>
|
||||
<set-body>{"error":{"code":"all_backends_unavailable","message":"Alle AI-backends er midlertidig utilgjengelige"}}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- Circuit breaker er obligatorisk for alle Azure OpenAI backends i produksjon -- uten det risikerer du cascading failures og bortkastet gateway-kapasitet mot throttlede backends.
|
||||
- Sett ALLTID `acceptRetryAfter: true` for Azure OpenAI backends. Azure OpenAI returnerer presise Retry-After verdier som gir optimal recovery-timing. Uten dette bruker du den statiske tripDuration som kan være for kort eller for lang.
|
||||
- Anbefalt baseline: `count: 3`, `interval: PT1M`, `tripDuration: PT10S`, `acceptRetryAfter: true`. Juster count opp for PAYGO-backends med mer toleranse.
|
||||
- Kombiner circuit breaker med backend pools for automatisk failover: når PTU-backend tripper, ruter APIM automatisk til PAYGO-backends uten klient-endringer.
|
||||
- Advarsler: Azure OpenAI kan returnere Retry-After opp til 86400 sekunder (1 dag). Sørg for at arkitekturen har nok alternative backends til å håndtere langvarige circuit breaks.
|
||||
|
|
@ -0,0 +1,448 @@
|
|||
# Cost Tracking & Chargeback via APIM Policies
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Når organisasjoner skalerer sin bruk av Azure OpenAI og andre AI-tjenester, blir kostnadssynlighet og tildeling av kostnader til riktig avdeling, prosjekt eller team en kritisk utfordring. Azure API Management (APIM) fungerer som et naturlig punkt for å samle inn kostnadsdata fra AI-modeller gjennom policyer som fanger token-bruk, modell-informasjon og forbruker-identitet. Denne informasjonen kan deretter brukes for intern fakturering (chargeback) og kostnadsoptimalisering.
|
||||
|
||||
For norsk offentlig sektor med stramme budsjetter og krav om transparens i ressursbruk er APIM-basert kostnadssporing spesielt verdifull. Mange statlige virksomheter deler AI-infrastruktur på tvers av avdelinger og prosjekter, og trenger mekanismer for å fordele kostnader rettferdig. Denne referansen dekker token-telling fra responser, modell-routing-tracking, chargeback-tagging, integrasjon med Azure Cost Management, og egendefinerte metriker.
|
||||
|
||||
APIM tilbyr innebygde policyer for å emittere token-metriker (`llm-emit-token-metric`) og logge LLM API-requests med fullstendig token-bruk. Kombinert med Azure Monitor, Application Insights og Cost Management gir dette en komplett pipeline for AI-kostnadssporing fra request til faktura.
|
||||
|
||||
---
|
||||
|
||||
## Token Counting from Responses
|
||||
|
||||
### Azure OpenAI Token Usage
|
||||
|
||||
Hver Azure OpenAI-respons inkluderer token-bruk i `usage`-feltet:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-abc123",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 150,
|
||||
"completion_tokens": 250,
|
||||
"total_tokens": 400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### llm-emit-token-metric Policy
|
||||
|
||||
Den primære policyen for å emittere token-metriker til Azure Monitor:
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<llm-emit-token-metric namespace="ai-cost-metrics">
|
||||
<!-- Dimensjoner for kostnadsallokering -->
|
||||
<dimension name="Subscription" value="@(context.Subscription.Name)" />
|
||||
<dimension name="API" value="@(context.Api.Name)" />
|
||||
<dimension name="Product" value="@(context.Product.Name)" />
|
||||
<dimension name="ClientIP" value="@(context.Request.IpAddress)" />
|
||||
<dimension name="Region" value="@(context.Deployment.Region)" />
|
||||
<dimension name="UserId"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("X-User-Id", "unknown"))" />
|
||||
<dimension name="Department"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("X-Department", "unassigned"))" />
|
||||
<dimension name="CostCenter"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("X-Cost-Center", "default"))" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### Token-typer og Kostnader
|
||||
|
||||
| Token-type | Beskrivelse | Kostnadsandel |
|
||||
|-----------|-------------|---------------|
|
||||
| Prompt tokens | Input-tokens (brukerens melding + system prompt) | Typisk 30-50% av kostnad |
|
||||
| Completion tokens | Output-tokens (modellens svar) | Typisk 50-70% av kostnad |
|
||||
| Cached tokens | Tokens fra prompt caching | Rabattert (opptil 50%) |
|
||||
| Total tokens | Sum av prompt + completion | Grunnlag for fakturering |
|
||||
|
||||
### Kostnadsberegning per Request
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Beregn estimert kostnad per request -->
|
||||
<set-variable name="estimated-cost-nok" value="@{
|
||||
// Eksempel: GPT-4o priser (tilpasses faktiske priser)
|
||||
var promptRate = 0.025m; // kr per 1000 prompt tokens
|
||||
var completionRate = 0.10m; // kr per 1000 completion tokens
|
||||
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
var usage = body?["usage"];
|
||||
if (usage == null) return "0";
|
||||
|
||||
var promptTokens = usage["prompt_tokens"]?.Value<decimal>() ?? 0;
|
||||
var completionTokens = usage["completion_tokens"]?.Value<decimal>() ?? 0;
|
||||
|
||||
var cost = (promptTokens / 1000m * promptRate) +
|
||||
(completionTokens / 1000m * completionRate);
|
||||
|
||||
return cost.ToString("F4");
|
||||
}" />
|
||||
|
||||
<!-- Legg til kostnadsinfo i response header -->
|
||||
<set-header name="X-Estimated-Cost-NOK" exists-action="override">
|
||||
<value>@((string)context.Variables["estimated-cost-nok"])</value>
|
||||
</set-header>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Routing Tracking
|
||||
|
||||
### Spore Hvilken Modell som Brukes
|
||||
|
||||
Når backend pools med forskjellige modeller brukes, er det viktig å spore hvilken modell og deployment som faktisk betjener requesten:
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Ekstraher modell-info fra respons -->
|
||||
<set-variable name="model-used" value="@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
return body?["model"]?.ToString() ?? "unknown";
|
||||
}" />
|
||||
|
||||
<set-variable name="deployment-id" value="@{
|
||||
return context.Request.MatchedParameters.GetValueOrDefault("deployment-id", "unknown");
|
||||
}" />
|
||||
|
||||
<!-- Emit metrikker med modell-dimensjon -->
|
||||
<llm-emit-token-metric namespace="ai-model-metrics">
|
||||
<dimension name="Model" value="@((string)context.Variables["model-used"])" />
|
||||
<dimension name="DeploymentId" value="@((string)context.Variables["deployment-id"])" />
|
||||
<dimension name="Backend" value="@(context.Request.Url.Host)" />
|
||||
<dimension name="DeploymentType"
|
||||
value="@(context.Request.Url.Host.Contains("ptu") ? "PTU" : "PayGo")" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### Modell-pris-mapping
|
||||
|
||||
| Modell | Prompt (kr/1K tokens) | Completion (kr/1K tokens) | Type |
|
||||
|--------|----------------------|--------------------------|------|
|
||||
| GPT-4o | 0.025 | 0.10 | Standard |
|
||||
| GPT-4o-mini | 0.0015 | 0.006 | Standard |
|
||||
| GPT-4 Turbo | 0.10 | 0.30 | Standard |
|
||||
| text-embedding-ada-002 | 0.001 | N/A | Embedding |
|
||||
| text-embedding-3-large | 0.0013 | N/A | Embedding |
|
||||
| PTU (alle modeller) | Fast pris/time | Fast pris/time | Provisioned |
|
||||
|
||||
**Merk:** Priser varierer og bør oppdateres jevnlig. PTU faktureres per time uavhengig av faktisk bruk.
|
||||
|
||||
---
|
||||
|
||||
## Chargeback Tagging
|
||||
|
||||
### Implementere Chargeback-modell
|
||||
|
||||
En effektiv chargeback-modell krever at hver AI-request tagges med identifiserbar informasjon:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- Ekstraher chargeback-info fra JWT token -->
|
||||
<set-variable name="department" value="@{
|
||||
var jwt = context.Request.Headers.GetValueOrDefault("Authorization", "")
|
||||
.Replace("Bearer ", "");
|
||||
if (string.IsNullOrEmpty(jwt)) return "unknown";
|
||||
var token = jwt.AsJwt();
|
||||
return token?.Claims.GetValueOrDefault("department", "unassigned");
|
||||
}" />
|
||||
|
||||
<set-variable name="cost-center" value="@{
|
||||
var jwt = context.Request.Headers.GetValueOrDefault("Authorization", "")
|
||||
.Replace("Bearer ", "");
|
||||
if (string.IsNullOrEmpty(jwt)) return "default";
|
||||
var token = jwt.AsJwt();
|
||||
return token?.Claims.GetValueOrDefault("cost_center", "default");
|
||||
}" />
|
||||
|
||||
<set-variable name="project-code" value="@{
|
||||
return context.Request.Headers.GetValueOrDefault("X-Project-Code", "general");
|
||||
}" />
|
||||
</inbound>
|
||||
|
||||
<outbound>
|
||||
<base />
|
||||
|
||||
<!-- Emit chargeback-metriker -->
|
||||
<llm-emit-token-metric namespace="chargeback-metrics">
|
||||
<dimension name="Department" value="@((string)context.Variables["department"])" />
|
||||
<dimension name="CostCenter" value="@((string)context.Variables["cost-center"])" />
|
||||
<dimension name="ProjectCode" value="@((string)context.Variables["project-code"])" />
|
||||
<dimension name="Subscription" value="@(context.Subscription.Name)" />
|
||||
<dimension name="Model" value="@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
return body?["model"]?.ToString() ?? "unknown";
|
||||
}" />
|
||||
</llm-emit-token-metric>
|
||||
|
||||
<!-- Legg til chargeback-headers i respons -->
|
||||
<set-header name="X-Chargeback-Department" exists-action="override">
|
||||
<value>@((string)context.Variables["department"])</value>
|
||||
</set-header>
|
||||
<set-header name="X-Chargeback-Tokens" exists-action="override">
|
||||
<value>@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
return body?["usage"]?["total_tokens"]?.ToString() ?? "0";
|
||||
}</value>
|
||||
</set-header>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### APIM Products for Chargeback
|
||||
|
||||
Bruk APIM Products for å gruppere API-tilgang per avdeling:
|
||||
|
||||
| Product | Beskrivelse | Rate Limit | Chargeback |
|
||||
|---------|-------------|-----------|-----------|
|
||||
| AI-Standard | Standard AI-tilgang | 10K TPM | Avdelingsbudsjett |
|
||||
| AI-Premium | Utvidet AI-tilgang | 50K TPM | Prosjektbudsjett |
|
||||
| AI-Unlimited | Full tilgang (admin) | Ubegrenset | Sentralt budsjett |
|
||||
|
||||
```xml
|
||||
<!-- Product-basert rate limiting -->
|
||||
<inbound>
|
||||
<base />
|
||||
<choose>
|
||||
<when condition="@(context.Product.Name == "AI-Standard")">
|
||||
<llm-token-limit counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="10000" />
|
||||
</when>
|
||||
<when condition="@(context.Product.Name == "AI-Premium")">
|
||||
<llm-token-limit counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000" />
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Azure Cost Management Integration
|
||||
|
||||
### Log Analytics for Kostnadsdata
|
||||
|
||||
Token-bruk logges til Azure Monitor via LLM API-logging:
|
||||
|
||||
```
|
||||
APIM → Diagnostic Settings → "Logs related to generative AI gateway"
|
||||
→ Log Analytics Workspace → ApiManagementGatewayLlmLog
|
||||
```
|
||||
|
||||
### KQL Query: Daglig Kostnad per Avdeling
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(30d)
|
||||
| extend Department = tostring(CustomDimensions["Department"])
|
||||
| extend Model = tostring(ModelDeployment)
|
||||
| extend PromptTokens = toint(PromptTokens)
|
||||
| extend CompletionTokens = toint(CompletionTokens)
|
||||
// Pris-mapping (oppdater etter faktiske priser)
|
||||
| extend PromptCostNOK = case(
|
||||
Model contains "gpt-4o-mini", PromptTokens * 0.0000015,
|
||||
Model contains "gpt-4o", PromptTokens * 0.000025,
|
||||
Model contains "gpt-4", PromptTokens * 0.0001,
|
||||
PromptTokens * 0.00001 // Default
|
||||
)
|
||||
| extend CompletionCostNOK = case(
|
||||
Model contains "gpt-4o-mini", CompletionTokens * 0.000006,
|
||||
Model contains "gpt-4o", CompletionTokens * 0.0001,
|
||||
Model contains "gpt-4", CompletionTokens * 0.0003,
|
||||
CompletionTokens * 0.00003 // Default
|
||||
)
|
||||
| extend TotalCostNOK = PromptCostNOK + CompletionCostNOK
|
||||
| summarize
|
||||
DailyCostNOK = sum(TotalCostNOK),
|
||||
TotalTokens = sum(toint(TotalTokens)),
|
||||
RequestCount = count()
|
||||
by Department, bin(TimeGenerated, 1d)
|
||||
| order by TimeGenerated desc, DailyCostNOK desc
|
||||
```
|
||||
|
||||
### KQL Query: Månedlig Chargeback-rapport
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > startofmonth(ago(0d))
|
||||
| extend CostCenter = tostring(CustomDimensions["CostCenter"])
|
||||
| extend Department = tostring(CustomDimensions["Department"])
|
||||
| extend Model = tostring(ModelDeployment)
|
||||
| summarize
|
||||
TotalPromptTokens = sum(toint(PromptTokens)),
|
||||
TotalCompletionTokens = sum(toint(CompletionTokens)),
|
||||
TotalTokens = sum(toint(TotalTokens)),
|
||||
RequestCount = count(),
|
||||
UniqueUsers = dcount(tostring(CustomDimensions["UserId"]))
|
||||
by CostCenter, Department, Model
|
||||
| extend EstimatedCostNOK =
|
||||
TotalPromptTokens * 0.000025 + TotalCompletionTokens * 0.0001
|
||||
| order by EstimatedCostNOK desc
|
||||
```
|
||||
|
||||
### Azure Workbook for Kostnadsdashboard
|
||||
|
||||
APIM tilbyr et innebygd Analytics-dashboard for LLM-APIer:
|
||||
|
||||
```
|
||||
1. APIM → Monitoring → Analytics → Language models
|
||||
2. Viser: Token consumption, Request count, Modell-fordeling
|
||||
3. Filtrer etter tidsperiode og API
|
||||
```
|
||||
|
||||
For egendefinert dashboard:
|
||||
|
||||
```
|
||||
1. Azure Monitor → Workbooks → New
|
||||
2. Legg til KQL-queries for chargeback
|
||||
3. Visualiser med tabeller, grafer, kart
|
||||
4. Del med stakeholders via Azure RBAC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Metrics
|
||||
|
||||
### Emit Custom Metriker med Policy Expressions
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
|
||||
<!-- Emit custom metrikk for estimert kostnad -->
|
||||
<emit-metric name="ai-estimated-cost"
|
||||
value="@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
var usage = body?["usage"];
|
||||
if (usage == null) return 0d;
|
||||
|
||||
var prompt = usage["prompt_tokens"]?.Value<double>() ?? 0;
|
||||
var completion = usage["completion_tokens"]?.Value<double>() ?? 0;
|
||||
|
||||
return (prompt * 0.000025) + (completion * 0.0001);
|
||||
}"
|
||||
namespace="ai-cost">
|
||||
<dimension name="Department"
|
||||
value="@((string)context.Variables.GetValueOrDefault("department", "unknown"))" />
|
||||
<dimension name="CostCenter"
|
||||
value="@((string)context.Variables.GetValueOrDefault("cost-center", "default"))" />
|
||||
</emit-metric>
|
||||
|
||||
<!-- Standard token-metriker -->
|
||||
<llm-emit-token-metric namespace="ai-tokens">
|
||||
<dimension name="Department"
|
||||
value="@((string)context.Variables.GetValueOrDefault("department", "unknown"))" />
|
||||
<dimension name="Model" value="@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
return body?["model"]?.ToString() ?? "unknown";
|
||||
}" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### Azure Monitor Alerts for Kostnadsoverskridelse
|
||||
|
||||
```bicep
|
||||
resource costAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
|
||||
name: 'ai-cost-threshold-alert'
|
||||
location: 'global'
|
||||
properties: {
|
||||
severity: 2
|
||||
evaluationFrequency: 'PT1H'
|
||||
windowSize: 'PT24H'
|
||||
criteria: {
|
||||
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
|
||||
allOf: [
|
||||
{
|
||||
name: 'DailyTokenBudgetExceeded'
|
||||
metricNamespace: 'ai-tokens'
|
||||
metricName: 'Total Tokens'
|
||||
operator: 'GreaterThan'
|
||||
threshold: 1000000 // 1M tokens per dag
|
||||
timeAggregation: 'Total'
|
||||
}
|
||||
]
|
||||
}
|
||||
actions: [
|
||||
{ actionGroupId: actionGroup.id }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Eksport til Power BI for Rapportering
|
||||
|
||||
```kusto
|
||||
// Eksporter data til Power BI via Log Analytics
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(90d)
|
||||
| project
|
||||
Timestamp = TimeGenerated,
|
||||
Department = tostring(CustomDimensions["Department"]),
|
||||
CostCenter = tostring(CustomDimensions["CostCenter"]),
|
||||
Model = ModelDeployment,
|
||||
PromptTokens = toint(PromptTokens),
|
||||
CompletionTokens = toint(CompletionTokens),
|
||||
TotalTokens = toint(TotalTokens),
|
||||
SubscriptionName = tostring(CustomDimensions["Subscription"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FinOps Integrasjon
|
||||
|
||||
### Azure Cost Management Tags
|
||||
|
||||
Kombiner APIM-metriker med Azure resource tags for helhetlig kostnadsbilde:
|
||||
|
||||
| Tag | Formål | Eksempel |
|
||||
|-----|--------|---------|
|
||||
| `Department` | Avdelingstilhørighet | "IT-seksjonen" |
|
||||
| `CostCenter` | Kostnadssenter-kode | "KS-4210" |
|
||||
| `Environment` | Miljø | "production" |
|
||||
| `Project` | Prosjektkode | "AI-chatbot-2026" |
|
||||
|
||||
### Kostnadsmodeller for AI
|
||||
|
||||
| Modell | Fordeler | Ulemper |
|
||||
|--------|---------|--------|
|
||||
| Per-token chargeback | Presis, rettferdig | Kompleks å administrere |
|
||||
| Flat rate per avdeling | Enkelt, forutsigbart | Urettferdig for lavbrukere |
|
||||
| Tier-basert (freemium) | Balansert, insentiverer effektivitet | Krever grensehåndtering |
|
||||
| PTU-allokering | Fast kostnad, forutsigbart | Ingen fleksibilitet |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [AI gateway in Azure API Management - Observability and governance](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities#observability-and-governance) — Oversikt over token-metriker
|
||||
- [llm-emit-token-metric policy](https://learn.microsoft.com/en-us/azure/api-management/llm-emit-token-metric-policy) — Policy-referanse for token-metriker
|
||||
- [Log token usage, prompts, and completions](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-llm-logs) — LLM API-logging
|
||||
- [Plan and manage costs for API Management](https://learn.microsoft.com/en-us/azure/api-management/plan-manage-costs) — APIM-kostnader i Cost Management
|
||||
- [Azure Cost Management overview](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/overview-cost-management) — Helhetlig kostnadsadministrasjon
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunder trenger å implementere kostnadssporing og intern fakturering for AI-tjenester gjennom APIM.
|
||||
- Start med `llm-emit-token-metric` policy med Department- og CostCenter-dimensjoner — dette gir umiddelbar synlighet i token-bruk per avdeling.
|
||||
- For norsk offentlig sektor: Anbefal tier-basert chargeback-modell med APIM Products som mapper til avdelingsbudsjetter. Flat rate er for enkelt, per-token er for komplekst for de fleste.
|
||||
- Husk at PTU-kostnader er faste per time — chargeback for PTU bør baseres på allokert kapasitet, ikke faktisk bruk.
|
||||
- Kombiner APIM-metriker med Azure resource tags for å gi et helhetlig bilde i Cost Management. APIM-metriker alene viser kun token-bruk, ikke infrastrukturkostnader.
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
# Developer Portal for AI API Discovery & Onboarding
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Azure API Managements Developer Portal er en automatisk generert, fullt tilpassbar nettside for API-dokumentasjon og selvbetjening. Nar organisasjoner eksponerer AI-modeller som API-er gjennom APIM, blir Developer Portal den sentrale plattformen der utviklere oppdager tilgjengelige AI-kapabiliteter, tester modeller interaktivt, administrerer API-nokler og overvaker eget forbruk. I tillegg tilbyr Azure API Center et komplementaert API-katalogverktoy.
|
||||
|
||||
For norsk offentlig sektor er en veladministrert Developer Portal viktig for a fremme gjenbruk av AI-tjenester pa tvers av etater og avdelinger. I samsvar med Digitaliseringsdirektoratets prinsipper om deling av data og tjenester, kan en offentlig tilgjengelig (eller intern) Developer Portal gi oversikt over tilgjengelige AI-API-er, redusere duplikering av arbeid og senke terskelen for a ta i bruk AI i nye prosjekter.
|
||||
|
||||
Developer Portal tilbyr ut av boksen: API-dokumentasjon med OpenAPI-spesifikasjoner, interaktiv testkonsoll, brukerregistrering og API-nokkelhondtering, samt bruksanalyse. Portalen kan tilpasses med egne stiler, innhold og branding -- og kan ogsa self-hostes for full kontroll.
|
||||
|
||||
---
|
||||
|
||||
## Portaltilpasning
|
||||
|
||||
### Tilpasningsomrader
|
||||
|
||||
| Omrade | Beskrivelse | Metode |
|
||||
|--------|-------------|--------|
|
||||
| Visuelt design | Farger, fonter, logo | Visual editor i Azure Portal |
|
||||
| Sidelayout | Menyer, sideoppsett, widgets | Drag-and-drop editor |
|
||||
| Egendefinert innhold | Sider, guider, FAQ | Markdown/HTML editor |
|
||||
| Widgets | API-liste, testconsole, profil | Konfigurerbare widgets |
|
||||
| Custom HTML/CSS | Full kontroll over utseende | Kode-editor |
|
||||
| Self-hosting | Full kontroll, egen infrastruktur | Open-source kodebase |
|
||||
|
||||
### Tilpasse Developer Portal for AI-API-er
|
||||
|
||||
Opprett dedikerte sider for AI-kapabiliteter:
|
||||
|
||||
**Eksempel: AI API Landing Page**
|
||||
|
||||
```html
|
||||
<!-- Custom page in Developer Portal -->
|
||||
<div class="ai-api-overview">
|
||||
<h1>AI API Gateway</h1>
|
||||
<p>Velkommen til var AI API Gateway. Her finner du dokumentasjon,
|
||||
testverktoy og tilgang til AI-modeller.</p>
|
||||
|
||||
<div class="ai-models-grid">
|
||||
<div class="model-card">
|
||||
<h3>GPT-4o Chat Completions</h3>
|
||||
<p>Generell chatbot og tekstgenerering</p>
|
||||
<ul>
|
||||
<li>Maks tokens: 128K kontekst</li>
|
||||
<li>Responstid: ~500ms</li>
|
||||
<li>Pris: Se lisensoversikt</li>
|
||||
</ul>
|
||||
<a href="/apis/chat-completions">Dokumentasjon</a>
|
||||
</div>
|
||||
|
||||
<div class="model-card">
|
||||
<h3>GPT-4o Mini</h3>
|
||||
<p>Raskere og rimeligere for enklere oppgaver</p>
|
||||
<ul>
|
||||
<li>Maks tokens: 128K kontekst</li>
|
||||
<li>Responstid: ~200ms</li>
|
||||
<li>Pris: 90% rimeligere enn GPT-4o</li>
|
||||
</ul>
|
||||
<a href="/apis/chat-completions-mini">Dokumentasjon</a>
|
||||
</div>
|
||||
|
||||
<div class="model-card">
|
||||
<h3>Embeddings API</h3>
|
||||
<p>Tekstembeddings for sok og analyse</p>
|
||||
<ul>
|
||||
<li>Modell: text-embedding-ada-002</li>
|
||||
<li>Dimensjoner: 1536</li>
|
||||
</ul>
|
||||
<a href="/apis/embeddings">Dokumentasjon</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Branding for norsk offentlig sektor
|
||||
|
||||
```css
|
||||
/* Custom CSS for public sector AI portal */
|
||||
:root {
|
||||
--portal-primary: #003366; /* Norwegian government blue */
|
||||
--portal-secondary: #C8102E; /* Norwegian flag red */
|
||||
--portal-background: #F5F5F5;
|
||||
--portal-text: #333333;
|
||||
--portal-font: 'Source Sans Pro', sans-serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--portal-primary);
|
||||
}
|
||||
|
||||
.api-card {
|
||||
border-left: 4px solid var(--portal-primary);
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API-dokumentasjon
|
||||
|
||||
### Best practices for AI API-dokumentasjon
|
||||
|
||||
| Seksjon | Innhold |
|
||||
|---------|---------|
|
||||
| Oversikt | Hva modellen kan, bruksomrader, begrensninger |
|
||||
| Autentisering | API-nokkel, OAuth 2.0, Managed Identity |
|
||||
| Endepunkter | URL-er, HTTP-metoder, parametere |
|
||||
| Request/Response | JSON-schemaer med eksempler |
|
||||
| Feilkoder | Standardiserte feilmeldinger |
|
||||
| Rate limits | Tokens per minutt, foresporsler per minutt |
|
||||
| Bruksretningslinjer | Ansvarlig bruk, innholdspolicy |
|
||||
| Kodeeksempler | Python, C#, JavaScript, curl |
|
||||
|
||||
### Legge til kodeeksempler i portalen
|
||||
|
||||
OpenAPI-spesifikasjonen kan berikes med eksempler:
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
/chat/completions:
|
||||
post:
|
||||
operationId: createChatCompletion
|
||||
summary: Create a chat completion
|
||||
description: |
|
||||
Genererer et chat completion-svar basert pa meldingshistorikk.
|
||||
Stotter bade system-, bruker- og assistentmeldinger.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChatCompletionRequest'
|
||||
examples:
|
||||
simple:
|
||||
summary: Enkel chatmelding
|
||||
value:
|
||||
model: gpt-4o
|
||||
messages:
|
||||
- role: user
|
||||
content: "Hva er Azure AI Foundry?"
|
||||
max_tokens: 500
|
||||
withSystem:
|
||||
summary: Med systemprompt
|
||||
value:
|
||||
model: gpt-4o
|
||||
messages:
|
||||
- role: system
|
||||
content: "Du er en norsk AI-assistent for offentlig sektor."
|
||||
- role: user
|
||||
content: "Forklar Schrems II for meg."
|
||||
max_tokens: 1000
|
||||
temperature: 0.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interaktiv testkonsoll
|
||||
|
||||
### Konfigurere testkonsoll for AI-API-er
|
||||
|
||||
Developer Portal inkluderer en interaktiv testkonsoll der utviklere kan:
|
||||
|
||||
1. Velge API-operasjon (f.eks. Chat Completions)
|
||||
2. Fylle inn parametere og request body
|
||||
3. Sende foresporselen direkte
|
||||
4. Se response inkludert token-forbruk
|
||||
|
||||
### Tilpasse testkonsollen
|
||||
|
||||
For AI-API-er er det nyttig a pre-populere request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Du er en hjelpsom assistent."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Skriv din melding her..."
|
||||
}
|
||||
],
|
||||
"max_tokens": 500,
|
||||
"temperature": 0.7
|
||||
}
|
||||
```
|
||||
|
||||
**Merk:** Testkonsollen bruker automatisk `Ocp-Apim-Subscription-Key` fra brukerens all-access-abonnement. For AI-API-er bor man begrense token-forbruk i test via rate limit policy.
|
||||
|
||||
### Rate limiting for testkonsoll
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Lower limits for test console requests -->
|
||||
<choose>
|
||||
<when condition="@(context.Request.Headers.GetValueOrDefault("Referer", "").Contains("developer"))">
|
||||
<rate-limit calls="10" renewal-period="60" />
|
||||
<set-header name="x-max-tokens-override" exists-action="override">
|
||||
<value>200</value>
|
||||
</set-header>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API-nokkelhondtering
|
||||
|
||||
### Abonnementsmodell for AI-API-er
|
||||
|
||||
APIM bruker Products og Subscriptions for tilgangskontroll:
|
||||
|
||||
| Produkt | Tilgang | Rate Limit | Bruksomrade |
|
||||
|---------|---------|-----------|-------------|
|
||||
| AI-Sandbox | Fri registrering | 100 tokens/min | Testing og utforskning |
|
||||
| AI-Standard | Godkjent | 10 000 tokens/min | Normal produksjon |
|
||||
| AI-Premium | Manuell godkjenning | 100 000 tokens/min | Hoyvolum-applikasjoner |
|
||||
| AI-Internal | Bare admin | Ubegrenset | Interne systemer |
|
||||
|
||||
### Bicep: Produktkonfigurasjon
|
||||
|
||||
```bicep
|
||||
resource sandboxProduct 'Microsoft.ApiManagement/service/products@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-sandbox'
|
||||
properties: {
|
||||
displayName: 'AI Sandbox'
|
||||
description: 'Fri tilgang til AI-API-er for testing. Begrenset til 100 tokens per minutt.'
|
||||
subscriptionRequired: true
|
||||
approvalRequired: false
|
||||
state: 'published'
|
||||
terms: 'Bruk kun til testing. Ikke send sensitiv informasjon.'
|
||||
}
|
||||
}
|
||||
|
||||
resource standardProduct 'Microsoft.ApiManagement/service/products@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-standard'
|
||||
properties: {
|
||||
displayName: 'AI Standard'
|
||||
description: 'Standard tilgang for godkjente applikasjoner. 10K tokens per minutt.'
|
||||
subscriptionRequired: true
|
||||
approvalRequired: true
|
||||
state: 'published'
|
||||
terms: 'Krever godkjenning. Folg retningslinjer for ansvarlig AI-bruk.'
|
||||
}
|
||||
}
|
||||
|
||||
resource premiumProduct 'Microsoft.ApiManagement/service/products@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-premium'
|
||||
properties: {
|
||||
displayName: 'AI Premium'
|
||||
description: 'Hoyvolum-tilgang for produksjonssystemer. 100K tokens per minutt.'
|
||||
subscriptionRequired: true
|
||||
approvalRequired: true
|
||||
state: 'published'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Brukerregistrering og selvbetjening
|
||||
|
||||
| Funksjon | Konfigurasjon |
|
||||
|----------|--------------|
|
||||
| Registrering | Azure AD / Microsoft Entra ID SSO |
|
||||
| Abonnementsgodkjenning | Manuell for Standard og Premium |
|
||||
| Automatisk nokkelrotasjon | Stottes via portal |
|
||||
| Bruksdashboard | Innebygd per abonnement |
|
||||
| Notifikasjoner | E-post ved godkjenning/avvisning |
|
||||
|
||||
---
|
||||
|
||||
## Selvbetjeningsarbeidsflyt for brukere
|
||||
|
||||
### Onboarding-prosess
|
||||
|
||||
```
|
||||
1. Bruker besaker Developer Portal
|
||||
2. Logger inn med Microsoft Entra ID (SSO)
|
||||
3. Blar gjennom tilgjengelige AI-API-er
|
||||
4. Velger produkt (Sandbox / Standard / Premium)
|
||||
5. Oppretter abonnement
|
||||
- Sandbox: Umiddelbar tilgang
|
||||
- Standard/Premium: Venter pa godkjenning
|
||||
6. Mottar API-nokkel (primaer + sekundaer)
|
||||
7. Tester i interaktiv konsoll
|
||||
8. Integrerer i applikasjon
|
||||
```
|
||||
|
||||
### Konfigurasjon av Developer Portal-tilgang
|
||||
|
||||
```xml
|
||||
<!-- Restrict developer portal access -->
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Require Azure AD authentication -->
|
||||
<validate-azure-ad-token tenant-id="{{TenantId}}">
|
||||
<client-application-ids>
|
||||
<application-id>{{DevPortalAppId}}</application-id>
|
||||
</client-application-ids>
|
||||
</validate-azure-ad-token>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Deaktivere offentlig registrering
|
||||
|
||||
For interne AI-portaler, deaktiver fri registrering og bruk Azure AD:
|
||||
|
||||
1. Ga til Developer Portal > Administrative interface
|
||||
2. Under **Identities**, fjern "Username and Password"
|
||||
3. Legg til "Azure Active Directory" som eneste identity provider
|
||||
4. Under **Settings**, deaktiver "Enable sign-up"
|
||||
|
||||
---
|
||||
|
||||
## Azure API Center: Komplementaer katalog
|
||||
|
||||
For storre organisasjoner kan Azure API Center brukes sammen med APIM Developer Portal:
|
||||
|
||||
| Egenskap | Developer Portal | API Center |
|
||||
|----------|-----------------|------------|
|
||||
| Hovedformal | Selvbetjening og testing | Organisatorisk katalog |
|
||||
| API-registrering | Fra APIM | Fra flere kilder |
|
||||
| MCP-server-registrering | Nei | Ja |
|
||||
| Governance-metadata | Begrenset | Omfattende |
|
||||
| Synkronisering | -- | Automatisk fra APIM |
|
||||
| Copilot Studio-connector | Nei | Ja |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Azure API Management Developer Portal overview](https://learn.microsoft.com/en-us/azure/api-management/developer-portal-overview) -- oversikt
|
||||
- [Tutorial: Access and customize the developer portal](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-developer-portal-customize) -- tilpasningsveiledning
|
||||
- [AI gateway - Developer experience](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities#developer-experience) -- AI-spesifikk developer experience
|
||||
- [What is Azure API Management?](https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts) -- APIM-oversikt
|
||||
- [Register and discover MCP servers in API Center](https://learn.microsoft.com/en-us/azure/api-center/register-discover-mcp-server) -- MCP i API Center
|
||||
- [Synchronize APIs between API Management and API Center](https://learn.microsoft.com/en-us/azure/api-center/synchronize-api-management-apis) -- synkronisering
|
||||
- [API Management subscriptions](https://learn.microsoft.com/en-us/azure/api-management/api-management-subscriptions) -- abonnementshondtering
|
||||
- [Self-host the developer portal](https://learn.microsoft.com/en-us/azure/api-management/developer-portal-self-host) -- self-hosting
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** nar kunden onsker a gjore AI-API-er tilgjengelige for interne eller eksterne utviklere med selvbetjening, dokumentasjon og tilgangskontroll.
|
||||
- For norsk offentlig sektor, anbefal alltid Microsoft Entra ID (Azure AD) som identity provider for Developer Portal -- unnga brukernavn/passord-registrering for bedre sikkerhet og sentral brukerstyring.
|
||||
- Kombiner APIM Developer Portal med Azure API Center for storre organisasjoner som har API-er fra flere kilder (ikke bare APIM) -- API Center gir en organisatorisk oversikt.
|
||||
- Anbefal en produkt-hierarki med Sandbox (fri tilgang, lav limit), Standard (godkjent, normal limit) og Premium (manuell godkjenning, hoy limit) for a gi kontrollert tilgang uten a bremse innovasjon.
|
||||
- Developer Portal er tilgjengelig i Developer, Basic, Standard og Premium tiers -- ikke i Consumption tier. For v2-tiers (Basic v2, Standard v2, Premium v2) er portalen tilgjengelig i alle bortsett fra Basic v2.
|
||||
|
|
@ -0,0 +1,627 @@
|
|||
# GenAI-Specific APIM Policies & Rules
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Azure API Management (APIM) inkluderer et sett med policyer spesifikt designet for generativ AI (GenAI). Disse policyene går utover tradisjonell API-gateway-funksjonalitet og adresserer unike utfordringer ved AI-workloads: content safety-modererering, prompt-validering, token-basert rate limiting, semantic caching, og audit-logging av prompts og completions. Samlet utgjør de kjernen i APIM sin AI gateway-kapabilitet.
|
||||
|
||||
For norsk offentlig sektor er GenAI-spesifikke policyer kritisk viktige. Krav fra AI Act, Datatilsynet og NSM innebarer at AI-systemer må ha mekanismer for innholdssikkerhet, logging for etterprøvbarhet, og kontroll over hva slags innhold som genereres. APIM-policyer gir disse kontrollene uten at hver enkelt applikasjon må implementere dem selv — en sentralisert, konsistent tilnærming til AI governance.
|
||||
|
||||
Denne referansen dekker alle GenAI-spesifikke APIM-policyer med fullstendige XML-eksempler, konfigurasjonsparametre og best practices. Policyene kan kombineres fritt i APIM sin inbound/outbound policy pipeline for å bygge en komplett AI safety-stack.
|
||||
|
||||
---
|
||||
|
||||
## Content Safety Integration
|
||||
|
||||
### llm-content-safety Policy
|
||||
|
||||
Policyen sender LLM-forespørsler til Azure AI Content Safety for moderering FØR de videresendes til backend-modellen:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<llm-content-safety backend-id="content-safety-backend"
|
||||
shield-prompt="true"
|
||||
enforce-on-completions="true">
|
||||
<categories output-type="EightSeverityLevels">
|
||||
<category name="Hate" threshold="4" />
|
||||
<category name="Violence" threshold="4" />
|
||||
<category name="SelfHarm" threshold="2" />
|
||||
<category name="Sexual" threshold="2" />
|
||||
</categories>
|
||||
<blocklists>
|
||||
<id>custom-blocklist-pii</id>
|
||||
<id>custom-blocklist-org-specific</id>
|
||||
</blocklists>
|
||||
</llm-content-safety>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### Prerequisites for Content Safety
|
||||
|
||||
```bicep
|
||||
// 1. Azure AI Content Safety ressurs
|
||||
resource contentSafety 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
|
||||
name: 'content-safety-service'
|
||||
location: 'westeurope'
|
||||
kind: 'ContentSafety'
|
||||
sku: { name: 'S0' }
|
||||
properties: {
|
||||
publicNetworkAccess: 'Disabled'
|
||||
}
|
||||
}
|
||||
|
||||
// 2. APIM Backend for Content Safety
|
||||
resource contentSafetyBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/content-safety-backend'
|
||||
properties: {
|
||||
url: 'https://content-safety-service.cognitiveservices.azure.com'
|
||||
protocol: 'http'
|
||||
credentials: {
|
||||
authorization: {
|
||||
scheme: 'managed-identity'
|
||||
parameter: 'https://cognitiveservices.azure.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Content Safety Konfigurasjon
|
||||
|
||||
| Attributt | Beskrivelse | Standard |
|
||||
|-----------|-------------|---------|
|
||||
| `backend-id` | Backend-entitet for Content Safety | Obligatorisk |
|
||||
| `shield-prompt` | Sjekk for adversarial attacks (jailbreak) | `false` |
|
||||
| `enforce-on-completions` | Sjekk også respons fra modellen | `false` |
|
||||
|
||||
### Kategorier og Terskelverider
|
||||
|
||||
| Kategori | Beskrivelse | Anbefalt terskel (offentlig sektor) |
|
||||
|----------|-------------|-------------------------------------|
|
||||
| `Hate` | Hatefullt innhold, diskriminering | 2-4 (streng) |
|
||||
| `Violence` | Voldelig innhold | 2-4 (streng) |
|
||||
| `SelfHarm` | Selvskading | 2 (svært streng) |
|
||||
| `Sexual` | Seksuelt innhold | 2 (svært streng) |
|
||||
|
||||
**Terskelskala:** 0 (mest restriktiv) til 7 (minst restriktiv). Lavere verdi = flere forespørsler blokkeres.
|
||||
|
||||
### Severity Level Output Types
|
||||
|
||||
| Output Type | Nivåer | Bruksområde |
|
||||
|------------|--------|------------|
|
||||
| `FourSeverityLevels` | 0, 2, 4, 6 | Standard, enklere policy |
|
||||
| `EightSeverityLevels` | 0-7 | Finkornet kontroll |
|
||||
|
||||
### Blokkert Request-respons
|
||||
|
||||
Når Content Safety blokkerer en forespørsel:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 403,
|
||||
"message": "Content safety violation detected. The request has been blocked."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt Validation Policies
|
||||
|
||||
### Custom Prompt Validation
|
||||
|
||||
Utover Azure AI Content Safety kan du implementere egne prompt-valideringsregler:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- Valider prompt-lengde -->
|
||||
<set-variable name="request-body" value="@{
|
||||
return context.Request.Body.As<JObject>(preserveContent: true);
|
||||
}" />
|
||||
|
||||
<choose>
|
||||
<!-- Blokkér ekstremt lange prompts -->
|
||||
<when condition="@{
|
||||
var body = (JObject)context.Variables["request-body"];
|
||||
var messages = body?["messages"] as JArray;
|
||||
if (messages == null) return false;
|
||||
var totalLength = messages.Sum(m => m["content"]?.ToString().Length ?? 0);
|
||||
return totalLength > 50000;
|
||||
}">
|
||||
<return-response>
|
||||
<set-status code="400" reason="Bad Request" />
|
||||
<set-body>{
|
||||
"error": {
|
||||
"code": "PromptTooLong",
|
||||
"message": "Total prompt length exceeds 50,000 characters."
|
||||
}
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
|
||||
<!-- Blokkér forespørsler uten system message -->
|
||||
<when condition="@{
|
||||
var body = (JObject)context.Variables["request-body"];
|
||||
var messages = body?["messages"] as JArray;
|
||||
if (messages == null) return true;
|
||||
return !messages.Any(m => m["role"]?.ToString() == "system");
|
||||
}">
|
||||
<return-response>
|
||||
<set-status code="400" reason="Bad Request" />
|
||||
<set-body>{
|
||||
"error": {
|
||||
"code": "SystemMessageRequired",
|
||||
"message": "A system message is required for all AI requests."
|
||||
}
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
|
||||
<!-- Blokkér forsøk på å overstyre system message -->
|
||||
<when condition="@{
|
||||
var body = (JObject)context.Variables["request-body"];
|
||||
var messages = body?["messages"] as JArray;
|
||||
if (messages == null) return false;
|
||||
var systemMessages = messages.Where(m => m["role"]?.ToString() == "system").ToList();
|
||||
return systemMessages.Count > 1;
|
||||
}">
|
||||
<return-response>
|
||||
<set-status code="400" reason="Bad Request" />
|
||||
<set-body>{
|
||||
"error": {
|
||||
"code": "MultipleSystemMessages",
|
||||
"message": "Only one system message is allowed per request."
|
||||
}
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### Inject Mandatory System Prompt
|
||||
|
||||
Tving en standard system prompt for alle forespørsler:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- Injiser organisasjonens standard system prompt -->
|
||||
<set-body>@{
|
||||
var body = context.Request.Body.As<JObject>(preserveContent: true);
|
||||
var messages = body["messages"] as JArray ?? new JArray();
|
||||
|
||||
// Organisasjonens mandatory system prompt
|
||||
var orgSystemPrompt = new JObject {
|
||||
["role"] = "system",
|
||||
["content"] = "You are a helpful assistant for Statens vegvesen. " +
|
||||
"You must respond in Norwegian unless explicitly asked otherwise. " +
|
||||
"Never share personal data, internal processes, or confidential information. " +
|
||||
"Always cite sources when providing factual information."
|
||||
};
|
||||
|
||||
// Fjern eksisterende system messages og legg inn organisasjonens
|
||||
var userMessages = new JArray(messages.Where(m => m["role"]?.ToString() != "system"));
|
||||
userMessages.Insert(0, orgSystemPrompt);
|
||||
body["messages"] = userMessages;
|
||||
|
||||
return body.ToString();
|
||||
}</set-body>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response Filtering
|
||||
|
||||
### Filtrere Sensitiv Informasjon fra Responser
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
|
||||
<!-- Fjern potensielle PII-lekkasjer fra AI-respons -->
|
||||
<choose>
|
||||
<when condition="@(!context.Response.Headers.GetValueOrDefault("Content-Type","")
|
||||
.Contains("text/event-stream"))">
|
||||
<set-body>@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
var choices = body?["choices"] as JArray;
|
||||
if (choices == null) return body.ToString();
|
||||
|
||||
foreach (var choice in choices)
|
||||
{
|
||||
var content = choice["message"]?["content"]?.ToString();
|
||||
if (content == null) continue;
|
||||
|
||||
// Fjern fødselsnumre (11 siffer)
|
||||
content = System.Text.RegularExpressions.Regex.Replace(
|
||||
content, @"\b\d{11}\b", "[REDACTED-PII]");
|
||||
|
||||
// Fjern e-postadresser
|
||||
content = System.Text.RegularExpressions.Regex.Replace(
|
||||
content, @"[\w.+-]+@[\w-]+\.[\w.-]+", "[REDACTED-EMAIL]");
|
||||
|
||||
// Fjern telefonnumre (norsk format)
|
||||
content = System.Text.RegularExpressions.Regex.Replace(
|
||||
content, @"\b(\+47)?\s?\d{2}\s?\d{2}\s?\d{2}\s?\d{2}\b", "[REDACTED-PHONE]");
|
||||
|
||||
choice["message"]["content"] = content;
|
||||
}
|
||||
|
||||
return body.ToString();
|
||||
}</set-body>
|
||||
</when>
|
||||
</choose>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### Legg til Disclaimer i Responser
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<set-header name="X-AI-Disclaimer" exists-action="override">
|
||||
<value>AI-generated content. Verify information before use.</value>
|
||||
</set-header>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting per Model
|
||||
|
||||
### Token Rate Limiting (llm-token-limit)
|
||||
|
||||
Begrens token-forbruk per forbruker, per modell:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- Global token-grense per subscription -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="10000"
|
||||
estimate-prompt-tokens="true"
|
||||
remaining-tokens-variable-name="remainingTokens" />
|
||||
|
||||
<!-- Ekstra grense per modell -->
|
||||
<choose>
|
||||
<when condition="@(context.Request.MatchedParameters["deployment-id"] == "gpt-4o")">
|
||||
<llm-token-limit
|
||||
counter-key="@("gpt4o-" + context.Subscription.Id)"
|
||||
tokens-per-minute="5000"
|
||||
estimate-prompt-tokens="true" />
|
||||
</when>
|
||||
<when condition="@(context.Request.MatchedParameters["deployment-id"] == "gpt-4o-mini")">
|
||||
<llm-token-limit
|
||||
counter-key="@("gpt4omini-" + context.Subscription.Id)"
|
||||
tokens-per-minute="20000"
|
||||
estimate-prompt-tokens="true" />
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### Token Quota (Periodisk)
|
||||
|
||||
Sett token-kvoter per dag, uke eller måned:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Daglig token-kvote per avdeling -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Request.Headers.GetValueOrDefault("X-Department", "default"))"
|
||||
tokens-per-minute="0"
|
||||
token-quota="100000"
|
||||
token-quota-period="86400"
|
||||
estimate-prompt-tokens="true"
|
||||
remaining-tokens-variable-name="dailyRemaining" />
|
||||
|
||||
<!-- Legg til gjenværende kvote i respons-header -->
|
||||
<set-header name="X-Daily-Tokens-Remaining" exists-action="override">
|
||||
<value>@(context.Variables.GetValueOrDefault<int>("dailyRemaining").ToString())</value>
|
||||
</set-header>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
### Prompt Token Pre-calculation
|
||||
|
||||
`estimate-prompt-tokens="true"` lar APIM estimere prompt-tokens FØR request sendes til backend. Hvis prompten allerede overskrider grensen, returneres 429 umiddelbart:
|
||||
|
||||
```
|
||||
Med pre-calculation:
|
||||
Klient → APIM (estimerer: 8000 tokens, grense: 5000) → 429 returnert
|
||||
→ Ingen request til Azure OpenAI → sparer backend-kapasitet
|
||||
|
||||
Uten pre-calculation:
|
||||
Klient → APIM → Azure OpenAI (bruker 8000 tokens) → Respons → APIM teller → Neste request: 429
|
||||
→ Tokens allerede brukt
|
||||
```
|
||||
|
||||
### Multi-Region Rate Limiting
|
||||
|
||||
**Viktig:** Rate limiting-policyer (`llm-token-limit`, `rate-limit`) teller SEPARAT per regional gateway i multi-region deployments:
|
||||
|
||||
| Policy | Scope | Multi-region oppførsel |
|
||||
|--------|-------|----------------------|
|
||||
| `llm-token-limit` | Per gateway | Separate tellere per region |
|
||||
| `rate-limit` | Per gateway | Separate tellere per region |
|
||||
| `quota` | Global (instans) | Én global teller |
|
||||
| `quota-by-key` | Global (instans) | Én global teller |
|
||||
|
||||
For å oppnå global rate limiting, bruk `quota-by-key` i stedet for `llm-token-limit`.
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging for Prompts
|
||||
|
||||
### Aktivere LLM API-logging
|
||||
|
||||
```
|
||||
1. APIM → Monitoring → Diagnostic settings
|
||||
2. "+ Add diagnostic setting"
|
||||
3. Velg "Logs related to generative AI gateway"
|
||||
4. Destination: Log Analytics workspace
|
||||
5. Save
|
||||
|
||||
6. APIM → APIs → [din API] → Settings → Diagnostic Logs
|
||||
7. Azure Monitor → Log LLM messages: Enabled
|
||||
8. Log prompts: 32768 bytes
|
||||
9. Log completions: 32768 bytes
|
||||
10. Save
|
||||
```
|
||||
|
||||
### Log-skjema: ApiManagementGatewayLlmLog
|
||||
|
||||
| Felt | Beskrivelse | Eksempel |
|
||||
|------|-------------|---------|
|
||||
| `TimeGenerated` | Tidspunkt for request | 2026-02-11T10:30:00Z |
|
||||
| `CorrelationId` | Unik request-ID | abc-123-def |
|
||||
| `OperationName` | API-operasjon | ChatCompletions |
|
||||
| `ModelDeployment` | Deployment-navn | gpt-4o |
|
||||
| `PromptTokens` | Antall prompt-tokens | 150 |
|
||||
| `CompletionTokens` | Antall completion-tokens | 250 |
|
||||
| `TotalTokens` | Totalt token-forbruk | 400 |
|
||||
| `RequestMessages` | Prompt-innhold (JSON) | [{"role":"user","content":"..."}] |
|
||||
| `ResponseMessages` | Completion-innhold (JSON) | [{"content":"..."}] |
|
||||
|
||||
### KQL: Audit Trail for AI-requests
|
||||
|
||||
```kusto
|
||||
// Full audit trail med prompt og respons
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(24h)
|
||||
| extend RequestArray = parse_json(RequestMessages)
|
||||
| extend ResponseArray = parse_json(ResponseMessages)
|
||||
| mv-expand RequestArray
|
||||
| mv-expand ResponseArray
|
||||
| project
|
||||
TimeGenerated,
|
||||
CorrelationId,
|
||||
Model = ModelDeployment,
|
||||
PromptTokens,
|
||||
CompletionTokens,
|
||||
Prompt = tostring(RequestArray.content),
|
||||
Response = tostring(ResponseArray.content)
|
||||
| summarize
|
||||
Input = strcat_array(make_list(Prompt), " "),
|
||||
Output = strcat_array(make_list(Response), " ")
|
||||
by CorrelationId, TimeGenerated, Model, PromptTokens, CompletionTokens
|
||||
| where isnotempty(Input) and isnotempty(Output)
|
||||
| order by TimeGenerated desc
|
||||
```
|
||||
|
||||
### KQL: Detektere Anomalier
|
||||
|
||||
```kusto
|
||||
// Finn uvanlig høy token-bruk per bruker
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(7d)
|
||||
| extend UserId = tostring(CustomDimensions["UserId"])
|
||||
| summarize
|
||||
AvgTokens = avg(toint(TotalTokens)),
|
||||
MaxTokens = max(toint(TotalTokens)),
|
||||
P95Tokens = percentile(toint(TotalTokens), 95),
|
||||
RequestCount = count()
|
||||
by UserId, bin(TimeGenerated, 1h)
|
||||
| where MaxTokens > 3 * AvgTokens // Flagg anomalier
|
||||
| order by MaxTokens desc
|
||||
```
|
||||
|
||||
### Event Hub-logging for Real-time Monitoring
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Logg til Event Hub for real-time analyse -->
|
||||
<log-to-eventhub logger-id="ai-audit-logger">@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
var usage = body?["usage"];
|
||||
|
||||
return new JObject(
|
||||
new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
|
||||
new JProperty("correlationId", context.RequestId),
|
||||
new JProperty("subscriptionId", context.Subscription?.Id),
|
||||
new JProperty("apiId", context.Api?.Id),
|
||||
new JProperty("model", body?["model"]?.ToString()),
|
||||
new JProperty("promptTokens", usage?["prompt_tokens"]),
|
||||
new JProperty("completionTokens", usage?["completion_tokens"]),
|
||||
new JProperty("totalTokens", usage?["total_tokens"]),
|
||||
new JProperty("statusCode", context.Response.StatusCode),
|
||||
new JProperty("region", context.Deployment.Region),
|
||||
new JProperty("latencyMs", context.Elapsed.TotalMilliseconds)
|
||||
).ToString();
|
||||
}</log-to-eventhub>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Komplett GenAI Policy Stack
|
||||
|
||||
### Full Inbound + Outbound Policy
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- 1. Autentisering -->
|
||||
<validate-azure-ad-token tenant-id="{{TENANT_ID}}"
|
||||
header-name="Authorization"
|
||||
failed-validation-httpcode="401" />
|
||||
|
||||
<!-- 2. Ekstraher brukerinfo for logging og rate limiting -->
|
||||
<set-variable name="caller-id"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("Authorization","")
|
||||
.AsJwt()?.Claims.GetValueOrDefault("oid", "anonymous"))" />
|
||||
<set-variable name="department"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("Authorization","")
|
||||
.AsJwt()?.Claims.GetValueOrDefault("department", "unknown"))" />
|
||||
|
||||
<!-- 3. Token rate limiting -->
|
||||
<llm-token-limit
|
||||
counter-key="@((string)context.Variables["caller-id"])"
|
||||
tokens-per-minute="10000"
|
||||
estimate-prompt-tokens="true" />
|
||||
|
||||
<!-- 4. Content Safety -->
|
||||
<llm-content-safety backend-id="content-safety-backend"
|
||||
shield-prompt="true">
|
||||
<categories output-type="EightSeverityLevels">
|
||||
<category name="Hate" threshold="4" />
|
||||
<category name="Violence" threshold="4" />
|
||||
<category name="SelfHarm" threshold="2" />
|
||||
<category name="Sexual" threshold="2" />
|
||||
</categories>
|
||||
</llm-content-safety>
|
||||
|
||||
<!-- 5. Semantic cache lookup -->
|
||||
<llm-semantic-cache-lookup
|
||||
score-threshold="0.9"
|
||||
embeddings-backend-id="embedding-backend"
|
||||
embeddings-backend-auth="system-assigned" />
|
||||
|
||||
<!-- 6. Backend med managed identity -->
|
||||
<set-backend-service backend-id="aoai-pool" />
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com"
|
||||
output-token-variable-name="mi-token" />
|
||||
<set-header name="Authorization" exists-action="override">
|
||||
<value>@("Bearer " + (string)context.Variables["mi-token"])</value>
|
||||
</set-header>
|
||||
</inbound>
|
||||
|
||||
<backend>
|
||||
<forward-request timeout="120"
|
||||
fail-on-error-status-code="true"
|
||||
buffer-response="false" />
|
||||
</backend>
|
||||
|
||||
<outbound>
|
||||
<base />
|
||||
|
||||
<!-- 7. Semantic cache store -->
|
||||
<llm-semantic-cache-store duration="3600" />
|
||||
|
||||
<!-- 8. Token-metriker -->
|
||||
<llm-emit-token-metric namespace="ai-metrics">
|
||||
<dimension name="UserId" value="@((string)context.Variables["caller-id"])" />
|
||||
<dimension name="Department" value="@((string)context.Variables["department"])" />
|
||||
<dimension name="API" value="@(context.Api.Name)" />
|
||||
<dimension name="Region" value="@(context.Deployment.Region)" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
|
||||
<on-error>
|
||||
<base />
|
||||
<return-response>
|
||||
<set-status code="500" reason="Internal Server Error" />
|
||||
<set-body>{
|
||||
"error": {
|
||||
"code": "GatewayError",
|
||||
"message": "An error occurred processing your AI request."
|
||||
}
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Policy Execution Order
|
||||
|
||||
```
|
||||
Inbound (fra topp til bunn):
|
||||
1. Authentication (validate-azure-ad-token)
|
||||
2. Variable extraction (set-variable)
|
||||
3. Token rate limiting (llm-token-limit)
|
||||
4. Content Safety (llm-content-safety)
|
||||
5. Cache lookup (llm-semantic-cache-lookup)
|
||||
6. Backend selection (set-backend-service)
|
||||
7. Backend auth (authentication-managed-identity)
|
||||
|
||||
Backend:
|
||||
8. Forward request (forward-request)
|
||||
|
||||
Outbound (fra topp til bunn):
|
||||
9. Cache store (llm-semantic-cache-store)
|
||||
10. Emit metrics (llm-emit-token-metric)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GenAI Policy Referanse
|
||||
|
||||
### Alle GenAI-spesifikke Policyer
|
||||
|
||||
| Policy | Fase | Formål |
|
||||
|--------|------|--------|
|
||||
| `llm-content-safety` | Inbound | Content Safety moderering |
|
||||
| `llm-token-limit` | Inbound | Token rate limiting |
|
||||
| `llm-semantic-cache-lookup` | Inbound | Semantic cache oppslag |
|
||||
| `llm-semantic-cache-store` | Outbound | Lagre i semantic cache |
|
||||
| `llm-emit-token-metric` | Outbound | Emitter token-metriker |
|
||||
|
||||
### Kompatibilitet
|
||||
|
||||
| Policy | Classic | V2 | Consumption | Self-hosted | Workspace |
|
||||
|--------|---------|-----|-------------|-------------|-----------|
|
||||
| `llm-content-safety` | Ja | Ja | Ja | Ja | Ja |
|
||||
| `llm-token-limit` | Ja | Ja | Ja | Ja | Ja |
|
||||
| `llm-semantic-cache-lookup` | Ja | Ja | Nei | Nei | Ja |
|
||||
| `llm-semantic-cache-store` | Ja | Ja | Nei | Nei | Ja |
|
||||
| `llm-emit-token-metric` | Ja | Ja | Ja | Ja | Ja |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) — Fullstendig oversikt over AI gateway-kapabiliteter
|
||||
- [Enforce content safety checks on LLM requests](https://learn.microsoft.com/en-us/azure/api-management/llm-content-safety-policy) — llm-content-safety policy referanse
|
||||
- [LLM token limit policy](https://learn.microsoft.com/en-us/azure/api-management/llm-token-limit-policy) — llm-token-limit policy referanse
|
||||
- [llm-emit-token-metric policy](https://learn.microsoft.com/en-us/azure/api-management/llm-emit-token-metric-policy) — Token-metrikk policy referanse
|
||||
- [llm-semantic-cache-lookup policy](https://learn.microsoft.com/en-us/azure/api-management/llm-semantic-cache-lookup-policy) — Semantic cache lookup referanse
|
||||
- [llm-semantic-cache-store policy](https://learn.microsoft.com/en-us/azure/api-management/llm-semantic-cache-store-policy) — Semantic cache store referanse
|
||||
- [Prompt Shields - Azure AI Content Safety](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/concepts/jailbreak-detection) — Prompt Shield-dokumentasjon
|
||||
- [Log token usage, prompts, and completions](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-llm-logs) — LLM-logging i APIM
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunder trenger å implementere AI safety og governance gjennom APIM-policyer, spesielt content safety, prompt-validering og audit-logging.
|
||||
- Den viktigste policyen for norsk offentlig sektor er `llm-content-safety` med `shield-prompt="true"` — dette blokkerer jailbreak-forsøk og uønsket innhold FØR det når modellen.
|
||||
- Husk rekkefølgen: Autentisering FØRST, deretter rate limiting, SÅ content safety, SÅ cache lookup. Content Safety koster tokens (kall til Content Safety API) — cache lookup etter content safety betyr at cachen kun inneholder "godkjent" innhold.
|
||||
- For audit-logging: Aktiver LLM API-logging i Diagnostic Settings. Dette gir full etterprøvbarhet for alle prompts og completions — noe som er påkrevd under AI Act for høy-risiko AI-systemer.
|
||||
- Rate limiting per modell er viktig: GPT-4o er dyrere enn GPT-4o-mini, og bør ha strengere token-grenser for å kontrollere kostnader.
|
||||
|
|
@ -0,0 +1,635 @@
|
|||
# Load Balancing Across Azure OpenAI Instances
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Lastbalansering på tvers av flere Azure OpenAI-instanser er en kritisk kapabilitet for enterprise AI-arkitekturer. Azure OpenAI har begrensninger på tokens per minutt (TPM) og requests per minutt (RPM) per deployment, og én enkelt instans vil sjelden dekke behovene til en hel organisasjon. Ved å distribuere trafikk over flere instanser -- gjerne i ulike regioner -- kan organisasjoner øke total kapasitet, forbedre tilgjengelighet og optimalisere kostnader.
|
||||
|
||||
Azure API Management (APIM) tilbyr innebygd backend pool-funksjonalitet som gjør dette uten egenutviklet kode. Backend pools støtter round-robin, weighted, priority-basert og session-aware lastbalansering, kombinert med circuit breaker for automatisk failover. For norsk offentlig sektor er dette spesielt relevant: flere etater kan dele infrastruktur, mens Provisioned Throughput Units (PTU) prioriteres for å maksimere investert kapasitet.
|
||||
|
||||
Det er viktig å forstå at APIM-lastbalansering er approksimasjon: ulike gateway-instanser synkroniserer ikke state seg imellom. Dette betyr at vektede fordelinger er omtrentlige, ikke eksakte. For de fleste AI-brukstilfeller er dette akseptabelt, da Azure OpenAI selv håndterer throttling med 429-responser og Retry-After headers.
|
||||
|
||||
---
|
||||
|
||||
## Backend Pool-konsepter
|
||||
|
||||
### Hva er en backend pool?
|
||||
|
||||
En backend pool i APIM er en samling av backend-tjenester som gatewayen behandler som én logisk enhet for lastbalansering. For Azure OpenAI betyr dette at flere OpenAI-instanser (potensielt i ulike regioner, med ulike deployment-typer) grupperes bak ett endepunkt.
|
||||
|
||||
| Egenskap | Verdi |
|
||||
|----------|-------|
|
||||
| Maks backends per pool | 30 |
|
||||
| Synkronisering mellom gateway-instanser | Nei (approksimasjon) |
|
||||
| Session awareness | Ja (cookie-basert) |
|
||||
| Health checking | Via circuit breaker |
|
||||
| Deployment-modell | Bicep, ARM, REST API, Portal |
|
||||
|
||||
### Backend-registrering
|
||||
|
||||
Hver backend i poolen registreres med URL, autentisering og valgfrie metadata:
|
||||
|
||||
```xml
|
||||
<!-- Policy: Rut til backend pool -->
|
||||
<set-backend-service backend-id="openai-pool" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Load Balancing-strategier
|
||||
|
||||
### Strategi 1: Round-Robin
|
||||
|
||||
Fordeler requests jevnt mellom alle backends i poolen.
|
||||
|
||||
```
|
||||
Request 1 → Backend A (Norway East)
|
||||
Request 2 → Backend B (Sweden Central)
|
||||
Request 3 → Backend C (West Europe)
|
||||
Request 4 → Backend A (Norway East) ← syklusen gjentas
|
||||
```
|
||||
|
||||
**Bruk når:**
|
||||
- Alle instanser har lik kapasitet (samme TPM-allokering)
|
||||
- Ingen preferanse for spesifikke regioner
|
||||
- Enkel konfigurasjon er prioritert
|
||||
|
||||
**Bicep:**
|
||||
|
||||
```bicep
|
||||
resource backendPool 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-pool'
|
||||
properties: {
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
id: '/backends/openai-norwayeast'
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-swedencentral'
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-westeurope'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strategi 2: Weighted (vektet)
|
||||
|
||||
Fordeler requests basert på tildelte vekter. Nyttig når backends har ulik kapasitet.
|
||||
|
||||
```
|
||||
Vekter: A=3, B=2, C=1 (totalt 6)
|
||||
→ A mottar ~50% av trafikk
|
||||
→ B mottar ~33% av trafikk
|
||||
→ C mottar ~17% av trafikk
|
||||
```
|
||||
|
||||
**Bruk når:**
|
||||
- Backends har ulik TPM-allokering
|
||||
- Blue-green deployment med gradvis trafikkskift
|
||||
- Ulike pricing-modeller (PTU vs. pay-as-you-go)
|
||||
|
||||
**Bicep:**
|
||||
|
||||
```bicep
|
||||
resource backendPool 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-pool-weighted'
|
||||
properties: {
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
id: '/backends/openai-norwayeast-ptu'
|
||||
weight: 5
|
||||
priority: 1
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-swedencentral-paygo'
|
||||
weight: 3
|
||||
priority: 1
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-westeurope-paygo'
|
||||
weight: 2
|
||||
priority: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strategi 3: Priority-Based (anbefalt for AI)
|
||||
|
||||
Organiserer backends i prioritetsgrupper. Lavere prioritetsnummer = høyere prioritet. Backends i lavere prioritetsgrupper brukes kun når alle backends i høyere grupper er utilgjengelige (circuit breaker utløst).
|
||||
|
||||
```
|
||||
Priority 1: PTU-instanser (fast pris, utnytt først)
|
||||
├── openai-norwayeast-ptu (weight: 3)
|
||||
└── openai-swedencentral-ptu (weight: 2)
|
||||
|
||||
Priority 2: Pay-as-you-go fallback
|
||||
├── openai-westeurope-paygo (weight: 1)
|
||||
└── openai-eastus-paygo (weight: 1)
|
||||
```
|
||||
|
||||
**Typisk scenario for norsk offentlig sektor:**
|
||||
|
||||
| Prioritet | Deployment-type | Region | Begrunnelse |
|
||||
|-----------|-----------------|--------|-------------|
|
||||
| 1 | PTU | Norway East | Datasuverenitet + fast pris, bruk først |
|
||||
| 1 | PTU | Sweden Central | Nær-region redundans |
|
||||
| 2 | Pay-as-you-go | West Europe | Spillover ved høy last |
|
||||
| 3 | Pay-as-you-go | East US | Nødfallback ved regional utfall |
|
||||
|
||||
**Bicep:**
|
||||
|
||||
```bicep
|
||||
resource backendPool 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-pool-priority'
|
||||
properties: {
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
id: '/backends/openai-norwayeast-ptu'
|
||||
weight: 3
|
||||
priority: 1
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-swedencentral-ptu'
|
||||
weight: 2
|
||||
priority: 1
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-westeurope-paygo'
|
||||
weight: 1
|
||||
priority: 2
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-eastus-paygo'
|
||||
weight: 1
|
||||
priority: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strategi 4: Session-Aware
|
||||
|
||||
Sikrer at alle requests fra samme bruker-sesjon rutes til samme backend. Kritisk for Azure OpenAI Assistants API der thread state er bundet til en spesifikk instans.
|
||||
|
||||
```
|
||||
Sesjon 1: Bruker A ──cookie──► Backend A (alle requests i sesjonen)
|
||||
Sesjon 2: Bruker B ──cookie──► Backend B (alle requests i sesjonen)
|
||||
Sesjon 3: Bruker C ──cookie──► Backend A (ny sesjon, tilfeldig valgt)
|
||||
```
|
||||
|
||||
**Bruk når:**
|
||||
- Assistants API (thread state)
|
||||
- Chat-applikasjoner med stateful backends
|
||||
- Når backend cacher bruker-kontekst
|
||||
|
||||
**Bicep:**
|
||||
|
||||
```bicep
|
||||
resource backendPool 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-pool-session'
|
||||
properties: {
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
id: '/backends/openai-norwayeast'
|
||||
weight: 1
|
||||
priority: 1
|
||||
}
|
||||
{
|
||||
id: '/backends/openai-swedencentral'
|
||||
weight: 1
|
||||
priority: 1
|
||||
}
|
||||
]
|
||||
sessionAffinity: {
|
||||
type: 'Cookie'
|
||||
cookieName: 'apim-session-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Cookie-håndtering for klienter:**
|
||||
Klienter MÅ lagre `Set-Cookie`-headeren fra APIM og sende den tilbake i påfølgende requests for å opprettholde sesjonsaffinitet.
|
||||
|
||||
---
|
||||
|
||||
## Individual Backend-konfigurasjon
|
||||
|
||||
### Registrere en Azure OpenAI backend
|
||||
|
||||
```bicep
|
||||
resource openaiBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-norwayeast-ptu'
|
||||
properties: {
|
||||
url: 'https://aoai-norwayeast.openai.azure.com/openai'
|
||||
protocol: 'http'
|
||||
description: 'Azure OpenAI PTU deployment i Norway East'
|
||||
circuitBreaker: {
|
||||
rules: [
|
||||
{
|
||||
name: 'openai-circuit-breaker'
|
||||
failureCondition: {
|
||||
count: 3
|
||||
interval: 'PT1M'
|
||||
statusCodeRanges: [
|
||||
{ min: 429, max: 429 }
|
||||
{ min: 500, max: 599 }
|
||||
]
|
||||
}
|
||||
tripDuration: 'PT10S'
|
||||
acceptRetryAfter: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Autentisering via Managed Identity
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com/" />
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
**RBAC-konfigurasjon:**
|
||||
|
||||
```bicep
|
||||
// Grant Cognitive Services User role to APIM managed identity
|
||||
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
||||
scope: openaiResource
|
||||
name: guid(apim.id, openaiResource.id, 'cognitive-services-user')
|
||||
properties: {
|
||||
roleDefinitionId: subscriptionResourceId(
|
||||
'Microsoft.Authorization/roleDefinitions',
|
||||
'a97b65f3-24c7-4388-baec-2e87135dc908' // Cognitive Services User
|
||||
)
|
||||
principalId: apim.identity.principalId
|
||||
principalType: 'ServicePrincipal'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment Slot Selection
|
||||
|
||||
### Routing til ulike modell-deployments
|
||||
|
||||
Når backends har ulike deployment-navn (f.eks. `gpt-4o` i en region, `gpt4-turbo` i en annen), kan APIM-policies transformere URL-en:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
|
||||
<!-- Override deployment name basert på backend-region -->
|
||||
<choose>
|
||||
<when condition="@(context.Backend?.AzureRegion == "norwayeast")">
|
||||
<rewrite-uri template="/deployments/gpt-4o/chat/completions" />
|
||||
</when>
|
||||
<when condition="@(context.Backend?.AzureRegion == "swedencentral")">
|
||||
<rewrite-uri template="/deployments/gpt4-turbo/chat/completions" />
|
||||
</when>
|
||||
</choose>
|
||||
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com/" />
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Modell-versjonshåndtering
|
||||
|
||||
```xml
|
||||
<!-- Sett api-version basert på backend -->
|
||||
<set-query-parameter name="api-version" exists-action="override">
|
||||
<value>2024-10-21</value>
|
||||
</set-query-parameter>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regional Distribution
|
||||
|
||||
### Topologi 1: Single-Region APIM, Multi-Region Backends
|
||||
|
||||
```
|
||||
APIM (Norway East)
|
||||
│
|
||||
┌────────┼────────┐
|
||||
▼ ▼ ▼
|
||||
OpenAI OpenAI OpenAI
|
||||
(Norway East) (Sweden C.) (West EU)
|
||||
Priority 1 Priority 1 Priority 2
|
||||
```
|
||||
|
||||
**Fordeler:** Enkel arkitektur, ett kontrollpunkt
|
||||
**Ulemper:** APIM er single point of failure, cross-region latens
|
||||
|
||||
### Topologi 2: Multi-Region APIM, Regional Backends
|
||||
|
||||
```
|
||||
Client → DNS (latency-based routing)
|
||||
│
|
||||
┌────────┴────────┐
|
||||
▼ ▼
|
||||
APIM Gateway APIM Gateway
|
||||
(Norway East) (Sweden Central)
|
||||
│ │
|
||||
▼ ▼
|
||||
OpenAI OpenAI
|
||||
(Norway East) (Sweden Central)
|
||||
```
|
||||
|
||||
**Fordeler:** Ingen regional single point of failure, lav latens
|
||||
**Ulemper:** Krever APIM Premium, dyrere
|
||||
|
||||
**Bicep for multi-region APIM:**
|
||||
|
||||
```bicep
|
||||
resource apim 'Microsoft.ApiManagement/service@2023-09-01-preview' = {
|
||||
name: 'apim-ai-gateway'
|
||||
location: 'norwayeast'
|
||||
sku: {
|
||||
name: 'Premium'
|
||||
capacity: 1
|
||||
}
|
||||
properties: {
|
||||
additionalLocations: [
|
||||
{
|
||||
location: 'swedencentral'
|
||||
sku: {
|
||||
name: 'Premium'
|
||||
capacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Topologi 3: Active-Active med Active-Passive Backends
|
||||
|
||||
Kombinerer regional redundans med kostnadsoptimalisering:
|
||||
|
||||
```
|
||||
APIM Gateway (Norway East)
|
||||
├── Active: OpenAI PTU (Norway East) Priority 1
|
||||
├── Passive: OpenAI PAYGO (Norway East) Priority 2
|
||||
└── Cross: OpenAI PAYGO (Sweden C.) Priority 3 (kun ved regional feil)
|
||||
|
||||
APIM Gateway (Sweden Central)
|
||||
├── Active: OpenAI PTU (Sweden C.) Priority 1
|
||||
├── Passive: OpenAI PAYGO (Sweden C.) Priority 2
|
||||
└── Cross: OpenAI PAYGO (Norway East) Priority 3
|
||||
```
|
||||
|
||||
**Regional policy routing:**
|
||||
|
||||
```xml
|
||||
<choose>
|
||||
<when condition="@(context.Deployment.Region == "Norway East")">
|
||||
<set-backend-service backend-id="pool-norwayeast" />
|
||||
</when>
|
||||
<when condition="@(context.Deployment.Region == "Sweden Central")">
|
||||
<set-backend-service backend-id="pool-swedencentral" />
|
||||
</when>
|
||||
</choose>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Throttling og Retry-håndtering
|
||||
|
||||
### Smart Load Balancing
|
||||
|
||||
Når en backend returnerer 429 (Too Many Requests), skal gatewayen:
|
||||
1. Lese `Retry-After`-headeren
|
||||
2. Markere backend som utilgjengelig via circuit breaker
|
||||
3. Umiddelbart retry til neste tilgjengelige backend i poolen
|
||||
4. IKKE vente (ingen delay mellom retries til ulike backends)
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
</inbound>
|
||||
|
||||
<backend>
|
||||
<retry condition="@(context.Response.StatusCode == 429)"
|
||||
count="3"
|
||||
interval="0"
|
||||
first-fast-retry="true">
|
||||
<set-backend-service backend-id="openai-pool-priority" />
|
||||
</retry>
|
||||
</backend>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### PTU + PAYGO Spillover-mønster
|
||||
|
||||
Det mest vanlige mønsteret for kostnadsoptimalisering:
|
||||
|
||||
```
|
||||
Normal trafikk:
|
||||
All trafikk → PTU (Priority 1, fast pris)
|
||||
|
||||
Ved throttling (PTU kapasitet brukt opp):
|
||||
Circuit breaker utløst på PTU
|
||||
Trafikk → PAYGO (Priority 2, pay-per-token)
|
||||
|
||||
Etter PTU recovery:
|
||||
Circuit breaker reset
|
||||
Trafikk → PTU (Priority 1, tilbake til fast pris)
|
||||
```
|
||||
|
||||
| Fase | Backend | Kostnad | Latens |
|
||||
|------|---------|---------|--------|
|
||||
| Normal | PTU | Fast (forutsigbar) | Lav (garantert) |
|
||||
| Spillover | PAYGO | Variabel (høyere) | Variabel |
|
||||
| Recovery | PTU | Fast | Lav |
|
||||
|
||||
---
|
||||
|
||||
## Komplett Bicep-eksempel
|
||||
|
||||
```bicep
|
||||
@description('Komplett AI Gateway med priority-based load balancing')
|
||||
|
||||
param location string = 'norwayeast'
|
||||
param environment string = 'prod'
|
||||
|
||||
// Azure OpenAI instances
|
||||
resource aoaiNorway 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
|
||||
name: 'aoai-norwayeast-${environment}'
|
||||
}
|
||||
|
||||
resource aoaiSweden 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
|
||||
name: 'aoai-swedencentral-${environment}'
|
||||
}
|
||||
|
||||
// APIM Instance
|
||||
resource apim 'Microsoft.ApiManagement/service@2023-09-01-preview' = {
|
||||
name: 'apim-ai-gw-${environment}'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'StandardV2'
|
||||
capacity: 1
|
||||
}
|
||||
identity: {
|
||||
type: 'SystemAssigned'
|
||||
}
|
||||
properties: {
|
||||
publisherEmail: 'ai-team@example.no'
|
||||
publisherName: 'AI Gateway'
|
||||
}
|
||||
}
|
||||
|
||||
// Backend: Norway East PTU
|
||||
resource backendNorwayPTU 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-norwayeast-ptu'
|
||||
properties: {
|
||||
url: '${aoaiNorway.properties.endpoint}openai'
|
||||
protocol: 'http'
|
||||
circuitBreaker: {
|
||||
rules: [
|
||||
{
|
||||
name: 'throttle-protection'
|
||||
failureCondition: {
|
||||
count: 3
|
||||
interval: 'PT1M'
|
||||
statusCodeRanges: [
|
||||
{ min: 429, max: 429 }
|
||||
{ min: 500, max: 599 }
|
||||
]
|
||||
}
|
||||
tripDuration: 'PT10S'
|
||||
acceptRetryAfter: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backend: Sweden Central PAYGO
|
||||
resource backendSwedenPAYGO 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-swedencentral-paygo'
|
||||
properties: {
|
||||
url: '${aoaiSweden.properties.endpoint}openai'
|
||||
protocol: 'http'
|
||||
circuitBreaker: {
|
||||
rules: [
|
||||
{
|
||||
name: 'throttle-protection'
|
||||
failureCondition: {
|
||||
count: 3
|
||||
interval: 'PT1M'
|
||||
statusCodeRanges: [
|
||||
{ min: 429, max: 429 }
|
||||
{ min: 500, max: 599 }
|
||||
]
|
||||
}
|
||||
tripDuration: 'PT10S'
|
||||
acceptRetryAfter: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backend Pool with priority-based routing
|
||||
resource backendPool 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'openai-pool'
|
||||
properties: {
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
id: '/backends/${backendNorwayPTU.name}'
|
||||
weight: 3
|
||||
priority: 1
|
||||
}
|
||||
{
|
||||
id: '/backends/${backendSwedenPAYGO.name}'
|
||||
weight: 1
|
||||
priority: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Overvåking og feilsøking
|
||||
|
||||
### Identifisere hvilken backend som serverte request
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<set-header name="X-Backend-Id" exists-action="override">
|
||||
<value>@(context.Backend?.Id ?? "unknown")</value>
|
||||
</set-header>
|
||||
<set-header name="X-Backend-Type" exists-action="override">
|
||||
<value>@(context.Backend?.Type ?? "unknown")</value>
|
||||
</set-header>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### KQL for load balancing-distribusjon
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLogs
|
||||
| where OperationId == "ChatCompletions_Create"
|
||||
| extend backendUrl = tostring(BackendUrl)
|
||||
| summarize RequestCount = count() by backendUrl, bin(TimeGenerated, 1h)
|
||||
| render columnchart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- Priority-based load balancing med PTU som Priority 1 og PAYGO som Priority 2 er det anbefalte mønsteret for enterprise AI-arkitekturer -- det maksimerer utnyttelsen av forhåndskjøpt kapasitet og faller automatisk tilbake til pay-per-use ved behov.
|
||||
- Backend pools er approksimerte: ulike gateway-instanser synkroniserer ikke, så vektede fordelinger er omtrentlige. For AI-workloads er dette akseptabelt fordi Azure OpenAI selv håndterer throttling med 429/Retry-After.
|
||||
- Session awareness er kritisk for Assistants API og chat-applikasjoner med stateful backends -- aktiver dette med cookie-basert sesjonsaffinitet i pool-konfigurasjonen.
|
||||
- For norsk offentlig sektor med datasuverenitetskrav: prioriter Norway East og Sweden Central, bruk private endpoints, og vurder om cross-region failover til EU-regioner er akseptabelt under gjeldende regelverk.
|
||||
- Kombiner alltid backend pools med circuit breaker (inkludert `acceptRetryAfter: true`) for intelligent failover ved 429-responser fra Azure OpenAI.
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
# Logging & Analytics for AI Traffic in APIM
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Observability er fundamentalt for a drifte AI-applikasjoner i produksjon. Azure API Management tilbyr omfattende logging- og analysekapabiliteter spesielt tilpasset AI-trafikk, inkludert token-sporring, prompt/completion-logging og innebygde dashboards for LLM-bruk. Disse verktoyene lar organisasjoner spore kostnader, overvake ytelse, sikre compliance og feilsoke problemer med AI-API-er.
|
||||
|
||||
For norsk offentlig sektor er logging og analytics spesielt viktig av flere grunner: Riksrevisjonen og Datatilsynet krever sporbarhet, offentlighetsloven krever dokumentasjon av automatiserte beslutninger, og budsjettkontroll krever presise kostnadsrapporter for AI-forbruk. APIM sin AI gateway gir de nodvendige verktoyene for a oppfylle disse kravene uten a bygge egne losninger.
|
||||
|
||||
APIM tilbyr to hovedkanaler for AI-logging: Application Insights-integrasjon for sanntidsmetrikker og Azure Monitor diagnostic settings for langtidslagring og analyse i Log Analytics. Begge kanalene stotter AI-spesifikke datapunkter som token-forbruk, modellnavn og valgfritt prompt/completion-innhold.
|
||||
|
||||
---
|
||||
|
||||
## Application Insights-integrasjon
|
||||
|
||||
### Oppsett av Application Insights Logger
|
||||
|
||||
1. Opprett eller koble til en Application Insights-ressurs
|
||||
2. Konfigurer logger i APIM
|
||||
3. Aktiver diagnostikk for spesifikke eller alle API-er
|
||||
|
||||
### Konfigurere logger med Bicep
|
||||
|
||||
```bicep
|
||||
resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = {
|
||||
name: appInsightsName
|
||||
}
|
||||
|
||||
resource apimLogger 'Microsoft.ApiManagement/service/loggers@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-gateway-logger'
|
||||
properties: {
|
||||
loggerType: 'applicationInsights'
|
||||
credentials: {
|
||||
connectionString: appInsights.properties.ConnectionString
|
||||
}
|
||||
resourceId: appInsights.id
|
||||
}
|
||||
}
|
||||
|
||||
resource apiDiagnostic 'Microsoft.ApiManagement/service/apis/diagnostics@2023-09-01-preview' = {
|
||||
parent: aiApi
|
||||
name: 'applicationinsights'
|
||||
properties: {
|
||||
loggerId: apimLogger.id
|
||||
alwaysLog: 'allErrors'
|
||||
logClientIp: true
|
||||
sampling: {
|
||||
samplingType: 'fixed'
|
||||
percentage: 100
|
||||
}
|
||||
frontend: {
|
||||
request: {
|
||||
headers: [ 'x-request-id', 'x-correlation-id', 'x-tenant-id' ]
|
||||
body: { bytes: 8192 }
|
||||
}
|
||||
response: {
|
||||
headers: [ 'x-model-used', 'x-cache-hit' ]
|
||||
body: { bytes: 8192 }
|
||||
}
|
||||
}
|
||||
backend: {
|
||||
request: {
|
||||
headers: [ 'Authorization' ]
|
||||
body: { bytes: 0 } // Don't log auth tokens
|
||||
}
|
||||
response: {
|
||||
body: { bytes: 8192 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Metrics med Token-sporring
|
||||
|
||||
### Emit Token Metrics Policy
|
||||
|
||||
APIM tilbyr dedikerte policies for a sende token-metrikker til Application Insights:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Emit token metrics for Azure OpenAI APIs -->
|
||||
<azure-openai-emit-token-metric namespace="ai-gateway-metrics">
|
||||
<dimension name="Subscription ID" value="@(context.Subscription.Id)" />
|
||||
<dimension name="API ID" value="@(context.Api.Id)" />
|
||||
<dimension name="Client IP" value="@(context.Request.IpAddress)" />
|
||||
</azure-openai-emit-token-metric>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
For andre LLM-API-er (ikke Azure OpenAI):
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Emit token metrics for generic LLM APIs -->
|
||||
<llm-emit-token-metric namespace="llm-metrics">
|
||||
<dimension name="Client IP" value="@(context.Request.IpAddress)" />
|
||||
<dimension name="API ID" value="@(context.Api.Id)" />
|
||||
<dimension name="User ID"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("x-user-id", "N/A"))" />
|
||||
<dimension name="Department"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("x-department", "unknown"))" />
|
||||
<dimension name="Application"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("x-app-id", "unknown"))" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Custom Metrics med emit-metric
|
||||
|
||||
For generelle metrikker utover token-sporring:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Emit custom request metrics -->
|
||||
<emit-metric name="ai-request-processed" value="1" namespace="ai-gateway">
|
||||
<dimension name="Model" value="@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
return body?["model"]?.ToString() ?? "unknown";
|
||||
}" />
|
||||
<dimension name="StatusCode" value="@(context.Response.StatusCode.ToString())" />
|
||||
<dimension name="CacheHit" value="@(context.Response.Headers.GetValueOrDefault("x-cache-hit", "false"))" />
|
||||
<dimension name="Subscription" value="@(context.Subscription?.Name ?? "unknown")" />
|
||||
</emit-metric>
|
||||
|
||||
<!-- Emit latency metric -->
|
||||
<emit-metric name="ai-backend-latency-ms" namespace="ai-gateway"
|
||||
value="@{
|
||||
var start = (DateTime)context.Variables["backendStartTime"];
|
||||
return ((DateTime.UtcNow - start).TotalMilliseconds).ToString();
|
||||
}">
|
||||
<dimension name="Model" value="@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
return body?["model"]?.ToString() ?? "unknown";
|
||||
}" />
|
||||
</emit-metric>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Begrensninger for custom metrics
|
||||
|
||||
| Begrensning | Verdi |
|
||||
|-------------|-------|
|
||||
| Maks dimensjoner per metric | 10 (5 default + 5 custom) |
|
||||
| Aktive tidsserier per region | 50 000 (innen 12-timers periode) |
|
||||
| Default dimensjoner (bruker 5) | Region, Service ID, Service Name, Service Type, + 1 reservert |
|
||||
| Tilgjengelige for custom | 5 dimensjoner |
|
||||
|
||||
---
|
||||
|
||||
## Token Tracking
|
||||
|
||||
### Diagnostics Setting for LLM Logs
|
||||
|
||||
Aktiver spesialisert LLM-logging via Azure Monitor diagnostic settings:
|
||||
|
||||
1. Ga til APIM-instansen i Azure Portal
|
||||
2. **Monitoring** > **Diagnostic settings** > **+ Add diagnostic setting**
|
||||
3. Velg **Logs related to generative AI gateway**
|
||||
4. Under Destination: **Send to Log Analytics workspace**
|
||||
|
||||
### Aktivere prompt/completion-logging per API
|
||||
|
||||
1. Velg API-en > **Settings** > **Diagnostic Logs** > **Azure Monitor**
|
||||
2. **Log LLM messages:** Enabled
|
||||
3. **Log prompts:** Velg og angi maks storrelse (f.eks. 32768 bytes)
|
||||
4. **Log completions:** Velg og angi maks storrelse (f.eks. 32768 bytes)
|
||||
|
||||
**Viktig:** Meldinger opp til 32 KB logges i en enkelt oppforing. Storre meldinger splittes i 32 KB-biter med sekvensnumre. Maks 2 MB per request/response.
|
||||
|
||||
### KQL-sporring: Join request og response
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| extend RequestArray = parse_json(RequestMessages)
|
||||
| extend ResponseArray = parse_json(ResponseMessages)
|
||||
| mv-expand RequestArray
|
||||
| mv-expand ResponseArray
|
||||
| project
|
||||
TimeGenerated,
|
||||
CorrelationId,
|
||||
OperationName,
|
||||
ModelDeploymentName,
|
||||
PromptTokens,
|
||||
CompletionTokens,
|
||||
TotalTokens,
|
||||
RequestContent = tostring(RequestArray.content),
|
||||
ResponseContent = tostring(ResponseArray.content)
|
||||
| summarize
|
||||
Input = strcat_array(make_list(RequestContent), " . "),
|
||||
Output = strcat_array(make_list(ResponseContent), " . "),
|
||||
PromptTokens = max(PromptTokens),
|
||||
CompletionTokens = max(CompletionTokens),
|
||||
TotalTokens = max(TotalTokens)
|
||||
by TimeGenerated, CorrelationId, OperationName, ModelDeploymentName
|
||||
| where isnotempty(Input) and isnotempty(Output)
|
||||
```
|
||||
|
||||
### KQL: Token-forbruk per applikasjon per dag
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(30d)
|
||||
| summarize
|
||||
TotalPromptTokens = sum(PromptTokens),
|
||||
TotalCompletionTokens = sum(CompletionTokens),
|
||||
TotalTokens = sum(TotalTokens),
|
||||
RequestCount = count()
|
||||
by bin(TimeGenerated, 1d), SubscriptionName = tostring(split(OperationName, "/")[0])
|
||||
| order by TimeGenerated desc
|
||||
```
|
||||
|
||||
### KQL: Modellbruk og kostnad
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(7d)
|
||||
| summarize
|
||||
PromptTokens = sum(PromptTokens),
|
||||
CompletionTokens = sum(CompletionTokens),
|
||||
Requests = count()
|
||||
by ModelDeploymentName
|
||||
| extend EstimatedCostUSD =
|
||||
case(
|
||||
ModelDeploymentName contains "gpt-4o",
|
||||
(PromptTokens / 1000000.0 * 2.5) + (CompletionTokens / 1000000.0 * 10.0),
|
||||
ModelDeploymentName contains "gpt-4o-mini",
|
||||
(PromptTokens / 1000000.0 * 0.15) + (CompletionTokens / 1000000.0 * 0.60),
|
||||
ModelDeploymentName contains "gpt-4",
|
||||
(PromptTokens / 1000000.0 * 30.0) + (CompletionTokens / 1000000.0 * 60.0),
|
||||
0.0
|
||||
)
|
||||
| extend EstimatedCostNOK = EstimatedCostUSD * 11.0
|
||||
| order by EstimatedCostNOK desc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Latency-overvaking
|
||||
|
||||
### Maling av end-to-end latency
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<set-variable name="requestStartTime" value="@(DateTime.UtcNow)" />
|
||||
</inbound>
|
||||
<backend>
|
||||
<base />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Calculate and expose latency -->
|
||||
<set-header name="x-total-latency-ms" exists-action="override">
|
||||
<value>@{
|
||||
var start = (DateTime)context.Variables["requestStartTime"];
|
||||
return ((DateTime.UtcNow - start).TotalMilliseconds).ToString("F0");
|
||||
}</value>
|
||||
</set-header>
|
||||
|
||||
<!-- Emit latency as custom metric -->
|
||||
<emit-metric name="ai-total-latency" namespace="ai-gateway"
|
||||
value="@{
|
||||
var start = (DateTime)context.Variables["requestStartTime"];
|
||||
return ((DateTime.UtcNow - start).TotalMilliseconds).ToString();
|
||||
}">
|
||||
<dimension name="API" value="@(context.Api.Name)" />
|
||||
<dimension name="StatusCode" value="@(context.Response.StatusCode.ToString())" />
|
||||
</emit-metric>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Latency-terskelvarsel
|
||||
|
||||
```kusto
|
||||
// Alert: AI API latency exceeds 5 seconds
|
||||
ApiManagementGatewayLogs
|
||||
| where TimeGenerated > ago(15m)
|
||||
| where ApiId contains "ai-gateway"
|
||||
| where ResponseTime > 5000
|
||||
| summarize
|
||||
Count = count(),
|
||||
AvgLatency = avg(ResponseTime),
|
||||
P95Latency = percentile(ResponseTime, 95)
|
||||
by bin(TimeGenerated, 5m), ApiId
|
||||
| where Count > 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Brukeratferdsanalyse
|
||||
|
||||
### Analytics Dashboard i APIM
|
||||
|
||||
APIM tilbyr et innebygd Azure Monitor-basert dashboard under **Monitoring > Analytics > Language models** med:
|
||||
|
||||
- Token-forbruk over tid
|
||||
- Fordeling per modell
|
||||
- Request-volum og feilrate
|
||||
- Gjennomsnittlig responstid
|
||||
|
||||
### KQL: Topp-brukere etter token-forbruk
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(7d)
|
||||
| summarize
|
||||
TotalTokens = sum(TotalTokens),
|
||||
Requests = count(),
|
||||
AvgTokensPerRequest = avg(TotalTokens)
|
||||
by SubscriptionId
|
||||
| order by TotalTokens desc
|
||||
| take 20
|
||||
```
|
||||
|
||||
### KQL: Populaere temaer (basert pa prompts)
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(7d)
|
||||
| extend RequestArray = parse_json(RequestMessages)
|
||||
| mv-expand RequestArray
|
||||
| where tostring(RequestArray.role) == "user"
|
||||
| extend UserMessage = tostring(RequestArray.content)
|
||||
| where strlen(UserMessage) > 10
|
||||
| extend Topic = case(
|
||||
UserMessage contains "azure" or UserMessage contains "cloud", "Azure/Cloud",
|
||||
UserMessage contains "kode" or UserMessage contains "code", "Programmering",
|
||||
UserMessage contains "sikkerhet" or UserMessage contains "security", "Sikkerhet",
|
||||
UserMessage contains "data" or UserMessage contains "database", "Data",
|
||||
"Annet"
|
||||
)
|
||||
| summarize Count = count() by Topic
|
||||
| order by Count desc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Eksport til Microsoft Foundry for modellevaluering
|
||||
|
||||
LLM-logger kan eksporteres som datasett for modellevaluering i Microsoft Foundry:
|
||||
|
||||
1. Join request/response med KQL (se over)
|
||||
2. Eksporter til CSV-format
|
||||
3. Last opp i Microsoft Foundry portal
|
||||
4. Kjor evaluering med innebygde eller egne metrikker
|
||||
|
||||
---
|
||||
|
||||
## Personvern og compliance
|
||||
|
||||
### Logging-policyer for norsk offentlig sektor
|
||||
|
||||
| Krav | Tiltak i APIM |
|
||||
|------|--------------|
|
||||
| GDPR Art. 5 (dataminimering) | Logg kun nodvendige felter, anonymiser PII |
|
||||
| Offentlighetsloven | Sikre sporbarhet for automatiserte beslutninger |
|
||||
| Datatilsynets retningslinjer | Ikke logg personopplysninger i prompts uten behandlingsgrunnlag |
|
||||
| Arkivloven | Langtidslagring i Log Analytics med retention policy |
|
||||
|
||||
### PII-filtrering i logging
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Sanitize prompts before logging -->
|
||||
<set-variable name="sanitizedRequest" value="@{
|
||||
var body = context.Request.Body.As<string>(preserveContent: true);
|
||||
// Remove Norwegian national ID (11 digits)
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b\d{11}\b", "[FODSELSNUMMER]");
|
||||
// Remove email addresses
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b[\w.-]+@[\w.-]+\.\w+\b", "[EMAIL]");
|
||||
return body;
|
||||
}" />
|
||||
|
||||
<trace source="ai-gateway" severity="information">
|
||||
<message>@((string)context.Variables["sanitizedRequest"])</message>
|
||||
</trace>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Log token usage, prompts, and completions for LLM APIs](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-llm-logs) -- hovedveiledning for LLM-logging
|
||||
- [AI gateway capabilities - Observability](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities#observability-and-governance) -- oversikt over observability
|
||||
- [How to integrate Azure API Management with Application Insights](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-app-insights) -- App Insights-integrasjon
|
||||
- [llm-emit-token-metric policy](https://learn.microsoft.com/en-us/azure/api-management/llm-emit-token-metric-policy) -- token-metrikk policy
|
||||
- [emit-metric policy](https://learn.microsoft.com/en-us/azure/api-management/emit-metric-policy) -- generell metrikk-policy
|
||||
- [Monitor API Management](https://learn.microsoft.com/en-us/azure/api-management/monitor-api-management) -- overordnet overvakning
|
||||
- [ApiManagementGatewayLlmLog table](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/apimanagementgatewayllmlog) -- Log Analytics-tabellreferanse
|
||||
- [Monitor AI agents with Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/agents-view) -- AI-agent-overvaking
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** nar kunden trenger a sette opp logging, dashboard eller kostnadsrapportering for sine AI-API-er, eller nar de ma oppfylle compliance-krav rundt sporbarhet av AI-bruk.
|
||||
- Anbefal alltid a aktivere bade Application Insights (sanntidsmetrikker) og diagnostic settings (Log Analytics for langtidsanalyse) -- de utfyller hverandre.
|
||||
- For kostnadsovervaking, bruk `llm-emit-token-metric` med dimensjoner for applikasjon, avdeling og abonnement -- dette gir granular kostnadstildeling uten manuell beregning.
|
||||
- Var oppmerksom pa personvern: Prompt-logging kan inneholde sensitiv informasjon. Anbefal PII-filtrering i policies for norsk offentlig sektor, og sorg for at lagringstid i Log Analytics samsvarer med organisasjonens retningslinjer.
|
||||
- KQL-sporringene i denne referansen kan brukes direkte i Azure Monitor Workbooks for a bygge tilpassede dashboards for ledelse og fagavdelinger.
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
# Multi-Region AI Gateway Architecture
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Organisasjoner som bygger AI-drevne tjenester med Azure OpenAI og andre LLM-tjenester trenger en gateway-arkitektur som tåler regionale feil, minimerer latens for geografisk distribuerte brukere, og overholder krav til dataresidency. Azure API Management (APIM) med multi-region deployment gir nettopp denne kapabiliteten, og er den anbefalte tilnærmingen for enterprise AI-workloads.
|
||||
|
||||
For norsk offentlig sektor er multi-region-design spesielt relevant: mange organisasjoner har krav om at data skal behandles innenfor EØS, men ønsker samtidig redundans og lav latens. APIM Premium-tier støtter multi-region gateways med én kontrollplan, noe som forenkler administrasjon og gir automatisk failover mellom regioner. Denne referansen dekker alle aspekter ved design, deploy og drift av en geografisk distribuert AI-gateway.
|
||||
|
||||
En vellykket multi-region AI-gateway-arkitektur balanserer tre hensyn: pålitelighet (at tjenesten overlever regionale feil), ytelse (at brukere opplever lav latens uavhengig av lokasjon), og compliance (at data behandles i henhold til regulatoriske krav). API Management løser alle tre gjennom innebygd FQDN-routing, regionale gateways og policy-basert trafikkhåndtering.
|
||||
|
||||
---
|
||||
|
||||
## Global APIM Distribution
|
||||
|
||||
### Multi-Region Deployment Architecture
|
||||
|
||||
APIM Premium-tier støtter replikering av gateway-komponenten til flere Azure-regioner. Kontrollplanet (management plane) og utviklerportalen forblir i primærregionen, mens gateway-trafikk håndteres lokalt i hver region.
|
||||
|
||||
| Komponent | Distribusjon | Merknader |
|
||||
|-----------|-------------|-----------|
|
||||
| Management plane | Kun primærregion | API-definisjoner, policyer, brukerhåndtering |
|
||||
| Developer portal | Kun primærregion | Brukerregistrering, API-dokumentasjon |
|
||||
| Gateway | Alle konfigurerte regioner | Håndterer API-trafikk, policy-kjøring |
|
||||
| Policy-konfigurasjon | Synkronisert (< 10 sek) | Automatisk propagering til alle regioner |
|
||||
|
||||
### Deployment via Azure Portal
|
||||
|
||||
```
|
||||
1. Naviger til APIM-instansen → Locations
|
||||
2. Klikk "+ Add" → Velg region (f.eks. North Europe)
|
||||
3. Konfigurer antall scale units
|
||||
4. Aktiver availability zones (anbefalt)
|
||||
5. Konfigurer VNet/subnet hvis nettverksintegrert
|
||||
6. Klikk "Add" → gjenta for flere regioner
|
||||
7. Klikk "Save" for å starte deployment
|
||||
```
|
||||
|
||||
### Bicep-template for Multi-Region APIM
|
||||
|
||||
```bicep
|
||||
resource apim 'Microsoft.ApiManagement/service@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim'
|
||||
location: 'westeurope'
|
||||
sku: {
|
||||
name: 'Premium'
|
||||
capacity: 2
|
||||
}
|
||||
properties: {
|
||||
publisherEmail: 'admin@example.com'
|
||||
publisherName: 'AI Gateway'
|
||||
additionalLocations: [
|
||||
{
|
||||
location: 'northeurope'
|
||||
sku: {
|
||||
name: 'Premium'
|
||||
capacity: 1
|
||||
}
|
||||
zones: ['1', '2', '3']
|
||||
}
|
||||
{
|
||||
location: 'swedencentral'
|
||||
sku: {
|
||||
name: 'Premium'
|
||||
capacity: 1
|
||||
}
|
||||
zones: ['1', '2', '3']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Regional DNS-mønster
|
||||
|
||||
Hver region får et eget DNS-endepunkt:
|
||||
|
||||
| Region | URL-mønster |
|
||||
|--------|------------|
|
||||
| Primary (West Europe) | `https://ai-gateway-apim.azure-api.net` |
|
||||
| West Europe (regional) | `https://ai-gateway-apim-westeurope-01.regional.azure-api.net` |
|
||||
| North Europe (regional) | `https://ai-gateway-apim-northeurope-01.regional.azure-api.net` |
|
||||
| Sweden Central (regional) | `https://ai-gateway-apim-swedencentral-01.regional.azure-api.net` |
|
||||
|
||||
---
|
||||
|
||||
## Region-Aware Routing
|
||||
|
||||
### Innebygd Latency-basert Routing
|
||||
|
||||
APIM tilbyr automatisk FQDN-basert routing som sender trafikk til gatewayen med lavest latens. Dette er standard oppførsel for multi-region deployments og krever ingen ekstra konfigurasjon.
|
||||
|
||||
```
|
||||
Klient → DNS-oppslag (ai-gateway-apim.azure-api.net)
|
||||
→ Latency-basert resolving → Nærmeste gateway
|
||||
→ Lokal policy-kjøring → Backend-kall
|
||||
```
|
||||
|
||||
### Routing til Regionale Backend-tjenester
|
||||
|
||||
For å utnytte geografisk distribusjon fullt ut, bør Azure OpenAI-instanser deployes i samme regioner som APIM-gateways. Bruk `context.Deployment.Region` for å rute til lokale backends:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<choose>
|
||||
<when condition="@("West Europe".Equals(context.Deployment.Region,
|
||||
StringComparison.OrdinalIgnoreCase))">
|
||||
<set-backend-service backend-id="aoai-westeurope" />
|
||||
</when>
|
||||
<when condition="@("North Europe".Equals(context.Deployment.Region,
|
||||
StringComparison.OrdinalIgnoreCase))">
|
||||
<set-backend-service backend-id="aoai-northeurope" />
|
||||
</when>
|
||||
<when condition="@("Sweden Central".Equals(context.Deployment.Region,
|
||||
StringComparison.OrdinalIgnoreCase))">
|
||||
<set-backend-service backend-id="aoai-swedencentral" />
|
||||
</when>
|
||||
<otherwise>
|
||||
<set-backend-service backend-id="aoai-westeurope" />
|
||||
</otherwise>
|
||||
</choose>
|
||||
</inbound>
|
||||
<backend>
|
||||
<base />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
</outbound>
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Backend Pool med Priority-basert Load Balancing
|
||||
|
||||
Kombinér regionale backends med priority groups for automatisk failover:
|
||||
|
||||
```bicep
|
||||
resource backendPool 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-pool-westeurope'
|
||||
properties: {
|
||||
description: 'West Europe pool med failover til North Europe'
|
||||
type: 'Pool'
|
||||
pool: {
|
||||
services: [
|
||||
{
|
||||
id: '/subscriptions/.../backends/aoai-westeurope'
|
||||
priority: 1
|
||||
weight: 1
|
||||
}
|
||||
{
|
||||
id: '/subscriptions/.../backends/aoai-northeurope'
|
||||
priority: 2
|
||||
weight: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Egendefinert Routing med Azure Traffic Manager
|
||||
|
||||
For scenarier der innebygd routing ikke er tilstrekkelig:
|
||||
|
||||
```
|
||||
1. Opprett Azure Traffic Manager-profil
|
||||
2. Konfigurer APIM regionale endepunkter som endpoints
|
||||
3. Bruk Geographic routing for data residency
|
||||
4. Konfigurer health probe mot /status-0123456789abcdef
|
||||
5. Pek custom domain mot Traffic Manager
|
||||
```
|
||||
|
||||
| Routing-metode | Bruksområde |
|
||||
|---------------|------------|
|
||||
| Geographic | Data residency-krav (EØS-region) |
|
||||
| Performance | Lavest latens for sluttbrukere |
|
||||
| Priority | DR-scenarier med primær/sekundær |
|
||||
| Weighted | Gradvis migrering mellom regioner |
|
||||
|
||||
---
|
||||
|
||||
## Latency Optimization
|
||||
|
||||
### Strategier for Lav Latens
|
||||
|
||||
| Strategi | Beskrivelse | Latensreduksjon |
|
||||
|----------|-------------|-----------------|
|
||||
| Co-lokalisering | APIM gateway + Azure OpenAI i samme region | Eliminerer cross-region latens |
|
||||
| Semantic caching | Cacher tidligere LLM-completions | 50-90% for gjentatte prompts |
|
||||
| Private endpoints | Direkte nettverksforbindelse uten offentlig internett | 10-30ms reduksjon |
|
||||
| Connection pooling | Gjenbruk av TCP-forbindelser | 50-100ms per request |
|
||||
| Regional DNS | Innebygd FQDN med latency-based routing | Automatisk optimal ruting |
|
||||
|
||||
### Semantic Caching med Azure Managed Redis
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<llm-semantic-cache-lookup
|
||||
score-threshold="0.9"
|
||||
embeddings-backend-id="embedding-backend"
|
||||
embeddings-backend-auth="system-assigned" />
|
||||
</inbound>
|
||||
<outbound>
|
||||
<base />
|
||||
<llm-semantic-cache-store duration="3600" />
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### Måling av Regional Latens
|
||||
|
||||
Bruk `llm-emit-token-metric` med regiondimensjon for å spore latens per region:
|
||||
|
||||
```xml
|
||||
<llm-emit-token-metric namespace="ai-gateway-metrics">
|
||||
<dimension name="Region" value="@(context.Deployment.Region)" />
|
||||
<dimension name="API" value="@(context.Api.Name)" />
|
||||
<dimension name="Backend" value="@(context.Request.Url.Host)" />
|
||||
</llm-emit-token-metric>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Residency Compliance
|
||||
|
||||
### EØS Data Residency-krav
|
||||
|
||||
For norsk offentlig sektor med krav om databehandling innenfor EØS:
|
||||
|
||||
| Krav | APIM-implementasjon |
|
||||
|------|---------------------|
|
||||
| Data-at-rest i EØS | Deploy APIM primærregion i West Europe/North Europe |
|
||||
| Data-in-transit i EØS | Private endpoints + VNet-isolasjon |
|
||||
| Ingen cross-geopolitical failover | Separate gateways per geopolitisk grense |
|
||||
| Logging i EØS | Log Analytics workspace i EØS-region |
|
||||
| Nøkkelhåndtering i EØS | Azure Key Vault i EØS-region |
|
||||
|
||||
### Viktige Advarsler
|
||||
|
||||
**Ikke** implementer en enhetlig gateway på tvers av geopolitiske regioner når data residency er påkrevd:
|
||||
|
||||
```
|
||||
RIKTIG:
|
||||
Gateway (West Europe) → Azure OpenAI (West Europe)
|
||||
Gateway (North Europe) → Azure OpenAI (North Europe)
|
||||
Separate FQDN per region
|
||||
|
||||
FEIL:
|
||||
Gateway (West Europe) → Azure OpenAI (East US) ← Bryter data residency
|
||||
Enhetlig gateway med failover til US-region ← Bryter data residency
|
||||
```
|
||||
|
||||
### Azure OpenAI Deployment Types og Data Residency
|
||||
|
||||
| Deployment Type | Data Residency | Egnet for offentlig sektor? |
|
||||
|----------------|---------------|---------------------------|
|
||||
| Standard | Data i angitt region | Ja, med EØS-region |
|
||||
| Provisioned (PTU) | Data i angitt region | Ja, med EØS-region |
|
||||
| Data Zone Standard | Data innenfor Azure data zone | Ja, med European data zone |
|
||||
| Global Standard | Data kan prosesseres i enhver region | Nei, ikke for data residency-krav |
|
||||
|
||||
### Policy for Data Residency Enforcement
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Blokkér requests som kan rutes utenfor EØS -->
|
||||
<choose>
|
||||
<when condition="@(!new[] { "West Europe", "North Europe",
|
||||
"Sweden Central", "France Central", "Germany West Central" }
|
||||
.Contains(context.Deployment.Region))">
|
||||
<return-response>
|
||||
<set-status code="403" reason="Forbidden" />
|
||||
<set-body>Data residency violation: Request routed outside EEA</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-Region Failover
|
||||
|
||||
### Automatisk Failover med Innebygd FQDN
|
||||
|
||||
Ved standard multi-region deployment håndterer APIM failover automatisk:
|
||||
|
||||
```
|
||||
1. Gateway i Region A svarer ikke
|
||||
2. DNS TTL utløper (typisk 5-10 minutter)
|
||||
3. Trafikk rutes til Region B (lavest latens blant aktive)
|
||||
4. Klienter MÅ respektere DNS TTL
|
||||
5. Retry-logikk i klient håndterer overgangsperiode
|
||||
```
|
||||
|
||||
### Disable/Enable Regional Gateway
|
||||
|
||||
For planlagt vedlikehold eller DR-testing:
|
||||
|
||||
```bash
|
||||
# Deaktiver gateway i North Europe
|
||||
az apim update \
|
||||
--name ai-gateway-apim \
|
||||
--resource-group rg-apim \
|
||||
--set additionalLocations[location="North Europe"].disableGateway=true
|
||||
|
||||
# Verifiser status
|
||||
az apim show \
|
||||
--name ai-gateway-apim \
|
||||
--resource-group rg-apim \
|
||||
--query "additionalLocations[].{Location:location,Disabled:disableGateway,Url:gatewayRegionalUrl}" \
|
||||
--output table
|
||||
|
||||
# Reaktiver etter vedlikehold
|
||||
az apim update \
|
||||
--name ai-gateway-apim \
|
||||
--resource-group rg-apim \
|
||||
--set additionalLocations[location="North Europe"].disableGateway=false
|
||||
```
|
||||
|
||||
### Active-Active med Active-Passive Azure OpenAI
|
||||
|
||||
For maksimal pålitelighet, kombinér active-active gateway med active-passive backend:
|
||||
|
||||
```
|
||||
Region A (Active):
|
||||
APIM Gateway → PTU Azure OpenAI (Priority 1)
|
||||
→ Standard Azure OpenAI (Priority 2, failover)
|
||||
|
||||
Region B (Active):
|
||||
APIM Gateway → PTU Azure OpenAI (Priority 1)
|
||||
→ Standard Azure OpenAI (Priority 2, failover)
|
||||
|
||||
Cross-region failover:
|
||||
Region A feil → All trafikk til Region B
|
||||
Region A PTU throttled → Standard deployment i Region A
|
||||
```
|
||||
|
||||
### Circuit Breaker for Backend Failover
|
||||
|
||||
```bicep
|
||||
resource backend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
name: 'ai-gateway-apim/aoai-westeurope'
|
||||
properties: {
|
||||
url: 'https://aoai-westeurope.openai.azure.com'
|
||||
protocol: 'http'
|
||||
circuitBreaker: {
|
||||
rules: [
|
||||
{
|
||||
failureCondition: {
|
||||
count: 3
|
||||
errorReasons: ['Server errors']
|
||||
interval: 'PT1M'
|
||||
statusCodeRanges: [
|
||||
{ min: 429, max: 429 }
|
||||
{ min: 500, max: 599 }
|
||||
]
|
||||
}
|
||||
name: 'aoai-breaker'
|
||||
tripDuration: 'PT30S'
|
||||
acceptRetryAfter: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kapasitetsplanlegging for Failover
|
||||
|
||||
Ved failover må gjenværende regioner absorbere all trafikk:
|
||||
|
||||
| Scenario | Region A Kapasitet | Region B Kapasitet | Nødvendig overprovisionering |
|
||||
|----------|-------------------|-------------------|-----------------------------|
|
||||
| 2 regioner, active-active | 100% normal load | 100% normal load | Hver region: 2x normal |
|
||||
| 2 regioner, active-passive | 100% normal load | 0% (standby) | Passiv region: 1x normal |
|
||||
| 3 regioner, active-active | 33% normal load | 33% normal load | Hver region: 1.5x normal |
|
||||
|
||||
Bruk [Azure OpenAI Capacity Calculator](https://oai.azure.com/portal/calculator) for PTU-kapasitetsplanlegging.
|
||||
|
||||
---
|
||||
|
||||
## Nettverksarkitektur
|
||||
|
||||
### Internal VNet Mode — Multi-Region
|
||||
|
||||
For scenarier med intern VNet-modus (typisk for offentlig sektor):
|
||||
|
||||
```
|
||||
Klient → Azure Front Door (WAF) → Private Endpoint → APIM Gateway (Region A)
|
||||
→ APIM Gateway (Region B)
|
||||
→ Egenhåndtert routing (Load Balancer/Traffic Manager)
|
||||
```
|
||||
|
||||
**Viktig:** I internal VNet-modus håndterer APIM IKKE automatisk routing mellom regionale gateways. Organisasjonen må selv implementere routing via Azure Front Door, Traffic Manager, eller en annen load balancer.
|
||||
|
||||
### VNet Krav per Region
|
||||
|
||||
Hver region krever eget VNet med nødvendige NSG-regler:
|
||||
|
||||
| Port | Retning | Formål |
|
||||
|------|---------|--------|
|
||||
| 3443 | Inbound | Management traffic |
|
||||
| 443 | Inbound | Client traffic (HTTPS) |
|
||||
| 1433 | Outbound | Azure SQL (primærregion) — påkrevd fra alle regioner |
|
||||
| 443 | Outbound | Azure Storage, Azure Monitor, Key Vault |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Deploy an Azure API Management instance to multiple Azure regions](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-deploy-multi-region) — Offisiell guide for multi-region deployment
|
||||
- [Use a gateway in front of multiple Azure OpenAI deployments or instances](https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/azure-openai-gateway-multi-backend) — Arkitekturmønstre for AI gateway
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) — Oversikt over AI gateway-kapabiliteter
|
||||
- [Access Azure OpenAI through a gateway](https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/azure-openai-gateway-guide) — Well-Architected Framework-veiledning
|
||||
- [Azure OpenAI deployment types](https://learn.microsoft.com/en-us/azure/ai-foundry/foundry-models/concepts/deployment-types) — Deployment types og data residency
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunder trenger en AI-gateway som dekker flere Azure-regioner, eller når data residency og failover er sentrale krav.
|
||||
- For norsk offentlig sektor: Anbefal alltid EØS-regioner (West Europe, North Europe, Sweden Central) og advar eksplisitt mot Global Standard deployments som kan prosessere data utenfor EØS.
|
||||
- Husk at rate-limiting policyer (rate-limit, llm-token-limit) teller separat per regional gateway — dette betyr at en 1000 TPM-grense gjelder per region, ikke totalt.
|
||||
- Start enkelt med to EØS-regioner (West Europe + North Europe) og vurder tredje region (Sweden Central) kun ved behov for høyere tilgjengelighet.
|
||||
- Kombiner alltid multi-region gateway med circuit breaker og backend pools for å sikre automatisk failover uten manuell intervensjon.
|
||||
|
|
@ -0,0 +1,571 @@
|
|||
# Request/Response Transformation for AI APIs
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Azure API Management (APIM) tilbyr over 75 innebygde policies for transformasjon av foresporsler og svar. Nar organisasjoner eksponerer AI-modeller gjennom APIM som AI gateway, blir transformasjon av request og response kritisk for a standardisere grensesnittet mellom ulike AI-backends (Azure OpenAI, Microsoft Foundry, tredjeparts LLM-er) og konsumerende applikasjoner. Ved a implementere model-agnostiske API-schemaer kan man bytte ut underliggende modeller uten a bryte klientkontrakter.
|
||||
|
||||
For norsk offentlig sektor er dette spesielt relevant: organisasjoner som Statens vegvesen, NAV og Skatteetaten kan etablere et standardisert AI-API-lag som abstraherer bort leverandoravhengigheter. Dette stotter prinsippet om leverandoruavhengighet fra Digitaliseringsdirektoratets arkitekturprinsipper, og gir fleksibilitet til a bytte mellom Azure OpenAI, Microsoft Foundry-modeller og fremtidige norske sprakmodeller uten endringer i klientapplikasjoner.
|
||||
|
||||
Transformasjonspolicies i APIM opererer i fire faser: inbound (request fra klient), backend (request til backend), outbound (response fra backend) og on-error. Denne referansen dekker praktiske monstre for a bygge et robust, model-agnostisk AI-API-lag med APIM-policies.
|
||||
|
||||
---
|
||||
|
||||
## Model-agnostiske API-schemaer
|
||||
|
||||
### Problemet med leverandorspesifikke API-er
|
||||
|
||||
Ulike AI-leverandorer bruker forskjellige API-formater:
|
||||
|
||||
| Leverandor | Endpoint-format | Auth-metode | Response-struktur |
|
||||
|------------|----------------|-------------|-------------------|
|
||||
| Azure OpenAI | `/openai/deployments/{id}/chat/completions` | API Key / Entra ID | `choices[].message.content` |
|
||||
| Microsoft Foundry | `/models/chat/completions` | Managed Identity | `choices[].message.content` |
|
||||
| Anthropic | `/v1/messages` | API Key | `content[].text` |
|
||||
| Google Vertex AI | `/v1/projects/{id}/locations/{loc}/publishers/google/models/{model}:predict` | OAuth 2.0 | `predictions[]` |
|
||||
| Open-source (vLLM) | `/v1/chat/completions` | Custom | `choices[].message.content` |
|
||||
|
||||
### Designmonster: Facade API Schema
|
||||
|
||||
Definer et internt standardskjema som alle AI-API-er mapper til:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "string",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system | user | assistant",
|
||||
"content": "string"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 1000,
|
||||
"top_p": 1.0
|
||||
},
|
||||
"metadata": {
|
||||
"request_id": "string",
|
||||
"tenant_id": "string",
|
||||
"application": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### APIM Policy: Route basert pa modellnavn
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Parse request body -->
|
||||
<set-variable name="requestBody"
|
||||
value="@(context.Request.Body.As<JObject>(preserveContent: true))" />
|
||||
<set-variable name="modelName"
|
||||
value="@(((JObject)context.Variables["requestBody"])["model"]?.ToString())" />
|
||||
|
||||
<!-- Route to correct backend based on model -->
|
||||
<choose>
|
||||
<when condition="@(((string)context.Variables["modelName"]).StartsWith("gpt-"))">
|
||||
<set-backend-service backend-id="azure-openai-backend" />
|
||||
<rewrite-uri template="/openai/deployments/{modelName}/chat/completions" />
|
||||
<set-query-parameter name="api-version" exists-action="override">
|
||||
<value>2024-08-01-preview</value>
|
||||
</set-query-parameter>
|
||||
</when>
|
||||
<when condition="@(((string)context.Variables["modelName"]).StartsWith("claude-"))">
|
||||
<set-backend-service backend-id="anthropic-backend" />
|
||||
<rewrite-uri template="/v1/messages" />
|
||||
</when>
|
||||
<otherwise>
|
||||
<set-backend-service backend-id="foundry-backend" />
|
||||
<rewrite-uri template="/models/chat/completions" />
|
||||
</otherwise>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Header Rewriting
|
||||
|
||||
### Autentiseringsheader-transformasjon
|
||||
|
||||
Nar APIM fungerer som AI gateway, ma den ofte transformere autentiseringsheadere mellom klientens format og backendets format:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Remove client API key and use managed identity -->
|
||||
<set-header name="api-key" exists-action="delete" />
|
||||
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
|
||||
|
||||
<!-- Authenticate with managed identity to Azure OpenAI -->
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com/"
|
||||
output-token-variable-name="msi-access-token" />
|
||||
<set-header name="Authorization" exists-action="override">
|
||||
<value>@("Bearer " + (string)context.Variables["msi-access-token"])</value>
|
||||
</set-header>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Tracking- og korrelasjonsheadere
|
||||
|
||||
For observability og sporbarhet, legg til standardiserte headere:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Add correlation headers -->
|
||||
<set-header name="x-request-id" exists-action="skip">
|
||||
<value>@(Guid.NewGuid().ToString())</value>
|
||||
</set-header>
|
||||
<set-header name="x-correlation-id" exists-action="skip">
|
||||
<value>@(context.RequestId.ToString())</value>
|
||||
</set-header>
|
||||
<set-header name="x-tenant-id" exists-action="override">
|
||||
<value>@(context.Subscription?.Name ?? "unknown")</value>
|
||||
</set-header>
|
||||
<set-header name="x-source-application" exists-action="override">
|
||||
<value>@(context.Request.Headers.GetValueOrDefault("x-app-id", "unspecified"))</value>
|
||||
</set-header>
|
||||
</inbound>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Forward correlation headers to client -->
|
||||
<set-header name="x-request-id" exists-action="override">
|
||||
<value>@(context.Request.Headers.GetValueOrDefault("x-request-id", ""))</value>
|
||||
</set-header>
|
||||
<set-header name="x-model-used" exists-action="override">
|
||||
<value>@{
|
||||
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
return body?["model"]?.ToString() ?? "unknown";
|
||||
}</value>
|
||||
</set-header>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Standard headere for AI-API-er
|
||||
|
||||
| Header | Retning | Formal |
|
||||
|--------|---------|--------|
|
||||
| `x-request-id` | Request/Response | Unik foresporsels-ID for sporing |
|
||||
| `x-correlation-id` | Request/Response | Korrelasjon pa tvers av tjenester |
|
||||
| `x-tenant-id` | Request | Identifiserer leietaker/abonnement |
|
||||
| `x-model-used` | Response | Hvilken modell som behandlet foresporselen |
|
||||
| `x-token-usage` | Response | Token-forbruk for fakturering |
|
||||
| `x-processing-time-ms` | Response | Backend-behandlingstid |
|
||||
| `x-rate-limit-remaining` | Response | Gjenverende rate limit |
|
||||
|
||||
---
|
||||
|
||||
## Payload-transformasjon
|
||||
|
||||
### Transformere request fra standardformat til leverandorspesifikt
|
||||
|
||||
Bruk `set-body` policy med Liquid-template eller C#-uttrykk:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Transform standard format to Anthropic API format -->
|
||||
<set-body>@{
|
||||
var inbound = context.Request.Body.As<JObject>();
|
||||
var messages = (JArray)inbound["messages"];
|
||||
string systemPrompt = "";
|
||||
var userMessages = new JArray();
|
||||
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
if (msg["role"]?.ToString() == "system")
|
||||
{
|
||||
systemPrompt = msg["content"]?.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
userMessages.Add(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var parameters = (JObject)inbound["parameters"] ?? new JObject();
|
||||
var transformed = new JObject
|
||||
{
|
||||
["model"] = inbound["model"],
|
||||
["max_tokens"] = parameters["max_tokens"] ?? 1024,
|
||||
["system"] = systemPrompt,
|
||||
["messages"] = userMessages
|
||||
};
|
||||
|
||||
if (parameters["temperature"] != null)
|
||||
transformed["temperature"] = parameters["temperature"];
|
||||
|
||||
return transformed.ToString();
|
||||
}</set-body>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Transformere response fra leverandorformat til standardformat
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Normalize Anthropic response to OpenAI-compatible format -->
|
||||
<choose>
|
||||
<when condition="@(context.Request.Headers.GetValueOrDefault("x-backend-type", "") == "anthropic")">
|
||||
<set-body>@{
|
||||
var response = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
var content = response["content"] as JArray;
|
||||
string text = content?[0]?["text"]?.ToString() ?? "";
|
||||
|
||||
var normalized = new JObject
|
||||
{
|
||||
["id"] = response["id"],
|
||||
["object"] = "chat.completion",
|
||||
["model"] = response["model"],
|
||||
["choices"] = new JArray
|
||||
{
|
||||
new JObject
|
||||
{
|
||||
["index"] = 0,
|
||||
["message"] = new JObject
|
||||
{
|
||||
["role"] = "assistant",
|
||||
["content"] = text
|
||||
},
|
||||
["finish_reason"] = response["stop_reason"]?.ToString() == "end_turn"
|
||||
? "stop" : response["stop_reason"]
|
||||
}
|
||||
},
|
||||
["usage"] = new JObject
|
||||
{
|
||||
["prompt_tokens"] = response["usage"]?["input_tokens"],
|
||||
["completion_tokens"] = response["usage"]?["output_tokens"],
|
||||
["total_tokens"] =
|
||||
(int)(response["usage"]?["input_tokens"] ?? 0) +
|
||||
(int)(response["usage"]?["output_tokens"] ?? 0)
|
||||
}
|
||||
};
|
||||
|
||||
return normalized.ToString();
|
||||
}</set-body>
|
||||
</when>
|
||||
</choose>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Response Normalization
|
||||
|
||||
### Standardisert feilformat
|
||||
|
||||
Ulike AI-backends returnerer feil i forskjellige formater. Normaliser til et konsistent format:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<on-error>
|
||||
<base />
|
||||
<set-header name="Content-Type" exists-action="override">
|
||||
<value>application/json</value>
|
||||
</set-header>
|
||||
|
||||
<!-- Map backend-specific errors to standard format -->
|
||||
<choose>
|
||||
<!-- Rate limit exceeded -->
|
||||
<when condition="@(context.Response.StatusCode == 429)">
|
||||
<set-body>@{
|
||||
var retryAfter = context.Response.Headers.GetValueOrDefault("Retry-After", "60");
|
||||
return new JObject
|
||||
{
|
||||
["error"] = new JObject
|
||||
{
|
||||
["code"] = "rate_limit_exceeded",
|
||||
["message"] = "Token eller request rate limit er overskredet. Prov igjen etter angitt tid.",
|
||||
["type"] = "rate_limit_error",
|
||||
["retry_after_seconds"] = int.Parse(retryAfter),
|
||||
["request_id"] = context.RequestId.ToString()
|
||||
}
|
||||
}.ToString();
|
||||
}</set-body>
|
||||
<set-status code="429" reason="Rate Limit Exceeded" />
|
||||
</when>
|
||||
|
||||
<!-- Model overloaded -->
|
||||
<when condition="@(context.Response.StatusCode == 503)">
|
||||
<set-body>@{
|
||||
return new JObject
|
||||
{
|
||||
["error"] = new JObject
|
||||
{
|
||||
["code"] = "model_overloaded",
|
||||
["message"] = "AI-modellen er midlertidig overbelastet. Foresporselen vil automatisk forsokes pa nytt.",
|
||||
["type"] = "server_error",
|
||||
["request_id"] = context.RequestId.ToString()
|
||||
}
|
||||
}.ToString();
|
||||
}</set-body>
|
||||
<set-status code="503" reason="Service Unavailable" />
|
||||
</when>
|
||||
|
||||
<!-- Content filter triggered -->
|
||||
<when condition="@(context.Response.StatusCode == 400 &&
|
||||
context.Response.Body.As<string>(preserveContent: true).Contains("content_filter"))">
|
||||
<set-body>@{
|
||||
return new JObject
|
||||
{
|
||||
["error"] = new JObject
|
||||
{
|
||||
["code"] = "content_filtered",
|
||||
["message"] = "Foresporselen ble blokkert av innholdsfilter. Vennligst reformuler.",
|
||||
["type"] = "content_policy_error",
|
||||
["request_id"] = context.RequestId.ToString()
|
||||
}
|
||||
}.ToString();
|
||||
}</set-body>
|
||||
<set-status code="400" reason="Content Filtered" />
|
||||
</when>
|
||||
|
||||
<!-- Generic error -->
|
||||
<otherwise>
|
||||
<set-body>@{
|
||||
return new JObject
|
||||
{
|
||||
["error"] = new JObject
|
||||
{
|
||||
["code"] = "internal_error",
|
||||
["message"] = "En uventet feil oppstod. Kontakt systemadministrator.",
|
||||
["type"] = "api_error",
|
||||
["status_code"] = context.Response.StatusCode,
|
||||
["request_id"] = context.RequestId.ToString()
|
||||
}
|
||||
}.ToString();
|
||||
}</set-body>
|
||||
<set-status code="500" reason="Internal Server Error" />
|
||||
</otherwise>
|
||||
</choose>
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Standard feilkoder for AI-API-er
|
||||
|
||||
| HTTP-kode | Feilkode | Beskrivelse |
|
||||
|-----------|----------|-------------|
|
||||
| 400 | `invalid_request` | Ugyldig foresporselsformat |
|
||||
| 400 | `content_filtered` | Innholdsfilter utlost |
|
||||
| 401 | `authentication_error` | Ugyldig eller manglende autentisering |
|
||||
| 403 | `authorization_error` | Ingen tilgang til denne modellen |
|
||||
| 404 | `model_not_found` | Modellen finnes ikke |
|
||||
| 429 | `rate_limit_exceeded` | For mange foresporsler |
|
||||
| 500 | `internal_error` | Intern serverfeil |
|
||||
| 503 | `model_overloaded` | Modellen er overbelastet |
|
||||
|
||||
---
|
||||
|
||||
## Versjonstranslasjon
|
||||
|
||||
### Handtere flere API-versjoner med transformasjon
|
||||
|
||||
Nar AI-API-er utvikler seg, kan APIM oversette mellom gammel og ny versjon:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<set-variable name="apiVersion"
|
||||
value="@(context.Request.Headers.GetValueOrDefault("api-version",
|
||||
context.Request.Url.Query.GetValueOrDefault("api-version", "2024-08-01")))" />
|
||||
|
||||
<!-- Transform v1 format to v2 format -->
|
||||
<choose>
|
||||
<when condition="@(((string)context.Variables["apiVersion"]).StartsWith("2023-"))">
|
||||
<set-body>@{
|
||||
var body = context.Request.Body.As<JObject>(preserveContent: true);
|
||||
|
||||
// v1 used "prompt" field, v2 uses "messages"
|
||||
if (body["prompt"] != null && body["messages"] == null)
|
||||
{
|
||||
var messages = new JArray
|
||||
{
|
||||
new JObject
|
||||
{
|
||||
["role"] = "user",
|
||||
["content"] = body["prompt"]
|
||||
}
|
||||
};
|
||||
body.Remove("prompt");
|
||||
body["messages"] = messages;
|
||||
}
|
||||
|
||||
// v1 used "max_tokens_to_sample", v2 uses "max_tokens"
|
||||
if (body["max_tokens_to_sample"] != null)
|
||||
{
|
||||
body["max_tokens"] = body["max_tokens_to_sample"];
|
||||
body.Remove("max_tokens_to_sample");
|
||||
}
|
||||
|
||||
return body.ToString();
|
||||
}</set-body>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Content validation for AI requests
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Validate required fields -->
|
||||
<choose>
|
||||
<when condition="@{
|
||||
var body = context.Request.Body.As<JObject>(preserveContent: true);
|
||||
return body?["messages"] == null || ((JArray)body["messages"]).Count == 0;
|
||||
}">
|
||||
<return-response>
|
||||
<set-status code="400" reason="Bad Request" />
|
||||
<set-header name="Content-Type" exists-action="override">
|
||||
<value>application/json</value>
|
||||
</set-header>
|
||||
<set-body>{"error":{"code":"invalid_request","message":"Field 'messages' is required and must be non-empty."}}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
|
||||
<!-- Enforce max message length -->
|
||||
<choose>
|
||||
<when condition="@(context.Request.Body.As<string>(preserveContent: true).Length > 128000)">
|
||||
<return-response>
|
||||
<set-status code="413" reason="Payload Too Large" />
|
||||
<set-header name="Content-Type" exists-action="override">
|
||||
<value>application/json</value>
|
||||
</set-header>
|
||||
<set-body>{"error":{"code":"payload_too_large","message":"Request body exceeds 128KB limit."}}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Policy Fragments for Reuse
|
||||
|
||||
APIM stotter policy fragments for gjenbruk av transformasjonslogikk:
|
||||
|
||||
```xml
|
||||
<!-- Fragment: ai-standard-headers -->
|
||||
<fragment>
|
||||
<set-header name="x-request-id" exists-action="skip">
|
||||
<value>@(Guid.NewGuid().ToString())</value>
|
||||
</set-header>
|
||||
<set-header name="x-correlation-id" exists-action="skip">
|
||||
<value>@(context.RequestId.ToString())</value>
|
||||
</set-header>
|
||||
<set-header name="x-timestamp" exists-action="override">
|
||||
<value>@(DateTime.UtcNow.ToString("o"))</value>
|
||||
</set-header>
|
||||
</fragment>
|
||||
```
|
||||
|
||||
Bruk fragmentet i policies:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<include-fragment fragment-id="ai-standard-headers" />
|
||||
<!-- Additional inbound policies -->
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bicep: Oppsett av transformasjons-API
|
||||
|
||||
```bicep
|
||||
resource apiManagement 'Microsoft.ApiManagement/service@2023-09-01-preview' existing = {
|
||||
name: apimName
|
||||
}
|
||||
|
||||
resource aiApi 'Microsoft.ApiManagement/service/apis@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-gateway-api'
|
||||
properties: {
|
||||
displayName: 'AI Gateway API'
|
||||
path: 'ai'
|
||||
protocols: [ 'https' ]
|
||||
subscriptionRequired: true
|
||||
subscriptionKeyParameterNames: {
|
||||
header: 'x-api-key'
|
||||
query: 'api-key'
|
||||
}
|
||||
apiType: 'http'
|
||||
}
|
||||
}
|
||||
|
||||
resource chatOperation 'Microsoft.ApiManagement/service/apis/operations@2023-09-01-preview' = {
|
||||
parent: aiApi
|
||||
name: 'chat-completions'
|
||||
properties: {
|
||||
displayName: 'Chat Completions'
|
||||
method: 'POST'
|
||||
urlTemplate: '/chat/completions'
|
||||
request: {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type'
|
||||
type: 'string'
|
||||
required: true
|
||||
defaultValue: 'application/json'
|
||||
}
|
||||
]
|
||||
}
|
||||
responses: [
|
||||
{
|
||||
statusCode: 200
|
||||
description: 'Successful completion'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Policies in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-policies) -- oversikt over policy-konseptet
|
||||
- [API Management policy reference - Transformation](https://learn.microsoft.com/en-us/azure/api-management/api-management-policies#transformation) -- komplett liste over transformasjonspolicies
|
||||
- [Set body policy](https://learn.microsoft.com/en-us/azure/api-management/set-body-policy) -- detaljert dokumentasjon for set-body
|
||||
- [Set header policy](https://learn.microsoft.com/en-us/azure/api-management/set-header-policy) -- header-manipulering
|
||||
- [Rewrite URI policy](https://learn.microsoft.com/en-us/azure/api-management/rewrite-uri-policy) -- URL-omskriving
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) -- AI gateway-oversikt
|
||||
- [Policy fragments in API Management](https://learn.microsoft.com/en-us/azure/api-management/policy-fragments) -- gjenbrukbare policy-fragmenter
|
||||
- [Tutorial: Transform and protect your API](https://learn.microsoft.com/en-us/azure/api-management/transform-api) -- hands-on tutorial
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** nar kunden onsker a bygge et model-agnostisk AI-API-lag som abstraherer bort leverandoravhengigheter, eller nar de trenger a standardisere feilhandtering pa tvers av AI-backends.
|
||||
- Anbefal alltid policy fragments for transformasjonslogikk som gjenbrukes pa tvers av flere API-er -- dette reduserer vedlikeholdsbyrden betydelig.
|
||||
- For norsk offentlig sektor, fremhev at model-agnostiske fasader stotter leverandoruavhengighet i trad med Digitaliseringsdirektoratets prinsipper.
|
||||
- Vurder a kombinere transformasjonspolicies med `validate-content` policy for a sikre at bade inngangs- og utgangsdata overholder definerte JSON-schemaer.
|
||||
- For organisasjoner som bruker flere AI-leverandorer (Azure OpenAI + Anthropic + open-source), er facade-monsteret med APIM en arkitekturforsterkning som gir fleksibilitet uten a eksponere backend-kompleksitet til konsumenter.
|
||||
|
|
@ -0,0 +1,531 @@
|
|||
# Security Hardening for AI Gateways in APIM
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Sikkerhet for AI-gateways krever en flerlagstilnaerming som dekker bade tradisjonelle API-sikkerhetstrusler og AI-spesifikke angrepsoverflater. Azure API Management som AI gateway tilbyr over 20 sikkerhetspolicies, fra IP-filtrering og sertifikatvalidering til AI-spesifikk innholdsmoderasjon og prompt injection-forebygging. En godt herdet AI gateway beskytter mot uautorisert tilgang, datalekkasje, prompt injection og misbruk av kostbare AI-ressurser.
|
||||
|
||||
For norsk offentlig sektor er sikkerhetsherding av AI-gateways obligatorisk gitt Datatilsynets retningslinjer for AI, NSMs grunnprinsipper for IKT-sikkerhet, Forvaltningslovens krav om forsvarlig saksbehandling, og EU AI Act som stiller krav til hoyrisiko-AI-systemer. En offentlig virksomhet som eksponerer AI-tjenester ma kunne dokumentere at tilstrekkelige sikkerhetstiltak er implementert pa alle nivaer.
|
||||
|
||||
Denne referansen dekker seks sikkerhetsomrader: nettverkstilgangskontroll, prompt injection-forebygging, PII-deteksjon og -maskering, mTLS-autentisering, revisjonssporing og compliance-kontroller. Hver seksjon inkluderer APIM policy XML-eksempler, Bicep-maler og anbefalinger for norsk offentlig sektor.
|
||||
|
||||
---
|
||||
|
||||
## IP-hvitelisting og -filtrering
|
||||
|
||||
### IP-filter policy
|
||||
|
||||
Begrens AI-API-tilgang til kjente IP-adresser eller nettverksomrader:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Allow only known IP ranges -->
|
||||
<ip-filter action="allow">
|
||||
<!-- Internal corporate network -->
|
||||
<address-range from="10.0.0.0" to="10.255.255.255" />
|
||||
<!-- VPN gateway -->
|
||||
<address>203.0.113.50</address>
|
||||
<!-- Azure Front Door backend IPs -->
|
||||
<address-range from="147.243.0.0" to="147.243.255.255" />
|
||||
<!-- Specific partner IPs -->
|
||||
<address>198.51.100.10</address>
|
||||
</ip-filter>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Dynamisk IP-filtrering med Named Values
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Use named values for maintainable IP lists -->
|
||||
<ip-filter action="allow">
|
||||
<address-range
|
||||
from="{{AllowedIpRangeStart}}"
|
||||
to="{{AllowedIpRangeEnd}}" />
|
||||
</ip-filter>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Nettverksisolering med VNet
|
||||
|
||||
For maksimal sikkerhet, deploy APIM i et virtuelt nettverk:
|
||||
|
||||
| Modus | Internett-tilgang | VNet-tilgang | Anbefalt for |
|
||||
|-------|-------------------|-------------|-------------|
|
||||
| External | Ja (gateway) | Ja | Innbyggertjenester med Front Door foran |
|
||||
| Internal | Nei | Ja | Rent interne AI-tjenester |
|
||||
| VNet Integration | Utgaende til VNet | Nei | Standard v2-tier |
|
||||
|
||||
```bicep
|
||||
resource apiManagement 'Microsoft.ApiManagement/service@2023-09-01-preview' = {
|
||||
name: apimName
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Premium'
|
||||
capacity: 1
|
||||
}
|
||||
properties: {
|
||||
virtualNetworkType: 'Internal' // Kun tilgjengelig via VNet
|
||||
virtualNetworkConfiguration: {
|
||||
subnetResourceId: apimSubnet.id
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt Injection-forebygging
|
||||
|
||||
### Forstar trusselen
|
||||
|
||||
Prompt injection er den mest kritiske AI-spesifikke trusselen (OWASP LLM Top 10 #1). Angripere injiserer instruksjoner i brukerinndata for a:
|
||||
- Overstyre systemprompt
|
||||
- Eksfiltrere sensitiv informasjon
|
||||
- Fa modellen til a utfore uautoriserte handlinger
|
||||
- Omga sikkerhetsmekanismer
|
||||
|
||||
### APIM Content Safety Policy
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Azure AI Content Safety for prompt moderation -->
|
||||
<llm-content-safety backend-id="content-safety-backend">
|
||||
<text-blocklist-ids>
|
||||
<id>prompt-injection-patterns</id>
|
||||
<id>offensive-content-no</id>
|
||||
</text-blocklist-ids>
|
||||
<categories>
|
||||
<category name="Hate" threshold="2" />
|
||||
<category name="Violence" threshold="2" />
|
||||
<category name="SelfHarm" threshold="2" />
|
||||
<category name="Sexual" threshold="2" />
|
||||
</categories>
|
||||
</llm-content-safety>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Policy-basert prompt injection-deteksjon
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Check for common prompt injection patterns -->
|
||||
<set-variable name="userMessage" value="@{
|
||||
var body = context.Request.Body.As<JObject>(preserveContent: true);
|
||||
var messages = (JArray)body?["messages"];
|
||||
if (messages == null) return "";
|
||||
|
||||
return string.Join(" ", messages
|
||||
.Where(m => m["role"]?.ToString() == "user")
|
||||
.Select(m => m["content"]?.ToString() ?? ""));
|
||||
}" />
|
||||
|
||||
<choose>
|
||||
<when condition="@{
|
||||
var msg = ((string)context.Variables["userMessage"]).ToLower();
|
||||
var injectionPatterns = new[] {
|
||||
"ignore previous instructions",
|
||||
"ignore all instructions",
|
||||
"disregard your system prompt",
|
||||
"you are now",
|
||||
"new instructions:",
|
||||
"override:",
|
||||
"forget everything",
|
||||
"system prompt:",
|
||||
"jailbreak",
|
||||
"do anything now",
|
||||
"developer mode"
|
||||
};
|
||||
return injectionPatterns.Any(p => msg.Contains(p));
|
||||
}">
|
||||
<return-response>
|
||||
<set-status code="400" reason="Bad Request" />
|
||||
<set-header name="Content-Type" exists-action="override">
|
||||
<value>application/json</value>
|
||||
</set-header>
|
||||
<set-body>@{
|
||||
return new JObject {
|
||||
["error"] = new JObject {
|
||||
["code"] = "content_policy_violation",
|
||||
["message"] = "Foresporselen ble blokkert av sikkerhetspolicy.",
|
||||
["request_id"] = context.RequestId.ToString()
|
||||
}
|
||||
}.ToString();
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
|
||||
<!-- Log potential injection attempts -->
|
||||
<choose>
|
||||
<when condition="@{
|
||||
var msg = ((string)context.Variables["userMessage"]).ToLower();
|
||||
var suspiciousPatterns = new[] {
|
||||
"system:", "assistant:", "[inst]", "<<sys>>",
|
||||
"\\n\\n", "```", "ignore", "pretend"
|
||||
};
|
||||
return suspiciousPatterns.Any(p => msg.Contains(p));
|
||||
}">
|
||||
<trace source="security" severity="warning">
|
||||
<message>@($"Suspicious prompt pattern from {context.Request.IpAddress}, sub: {context.Subscription?.Name}")</message>
|
||||
</trace>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Microsoft Prompt Shields
|
||||
|
||||
For avansert beskyttelse, bruk Microsoft Prompt Shields (via Microsoft Entra Global Secure Access):
|
||||
|
||||
| Funksjon | Beskrivelse |
|
||||
|----------|-------------|
|
||||
| Jailbreak-deteksjon | Identifiserer forsok pa a omga sikkerhetsinstruksjoner |
|
||||
| Indirect injection | Oppdager injeksjon via dokumenter eller URLs |
|
||||
| Data exfiltration | Blokkerer forsok pa a trekke ut data |
|
||||
| Nettverksniva-enforcement | Fungerer uavhengig av applikasjonskode |
|
||||
|
||||
---
|
||||
|
||||
## PII-deteksjon og -maskering
|
||||
|
||||
### PII-filtrering i inbound requests
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Detect and mask PII in prompts -->
|
||||
<set-variable name="sanitizedBody" value="@{
|
||||
var body = context.Request.Body.As<string>(preserveContent: true);
|
||||
|
||||
// Norwegian national ID (fodselsnummer) - 11 digits
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b(\d{2})(0[1-9]|1[0-2])(\d{2})\d{5}\b", "$1$2$3*****");
|
||||
|
||||
// Email addresses
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b[\w.+-]+@[\w.-]+\.\w{2,}\b", "[EMAIL]");
|
||||
|
||||
// Norwegian phone numbers
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b(?:\+47|0047)?\s*(?:\d\s*){8}\b", "[TELEFON]");
|
||||
|
||||
// Credit card numbers (basic pattern)
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", "[KORTNUMMER]");
|
||||
|
||||
// Bank account numbers (Norwegian format)
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b\d{4}\.\d{2}\.\d{5}\b", "[KONTONUMMER]");
|
||||
|
||||
return body;
|
||||
}" />
|
||||
|
||||
<!-- Replace request body with sanitized version -->
|
||||
<set-body>@((string)context.Variables["sanitizedBody"])</set-body>
|
||||
|
||||
<!-- Log if PII was detected -->
|
||||
<choose>
|
||||
<when condition="@{
|
||||
var original = context.Request.Body.As<string>(preserveContent: true);
|
||||
var sanitized = (string)context.Variables["sanitizedBody"];
|
||||
return original != sanitized;
|
||||
}">
|
||||
<trace source="pii-detection" severity="warning">
|
||||
<message>@($"PII detected and masked in request from {context.Subscription?.Name}")</message>
|
||||
</trace>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### PII-filtrering i outbound responses
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Mask PII in AI model responses -->
|
||||
<set-body>@{
|
||||
var body = context.Response.Body.As<string>(preserveContent: true);
|
||||
|
||||
// Apply same PII patterns as inbound
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b(\d{2})(0[1-9]|1[0-2])(\d{2})\d{5}\b", "$1$2$3*****");
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b[\w.+-]+@[\w.-]+\.\w{2,}\b", "[EMAIL]");
|
||||
body = System.Text.RegularExpressions.Regex.Replace(
|
||||
body, @"\b(?:\+47|0047)?\s*(?:\d\s*){8}\b", "[TELEFON]");
|
||||
|
||||
return body;
|
||||
}</set-body>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### PII-deteksjonskategorier
|
||||
|
||||
| Kategori | Monster | Eksempel |
|
||||
|----------|---------|---------|
|
||||
| Fodselsnummer | `\d{11}` | 01019012345 |
|
||||
| E-postadresse | standard e-post regex | ola@eksempel.no |
|
||||
| Telefonnummer | +47 / 8 siffer | +47 912 34 567 |
|
||||
| Kortnummer | 16 siffer | 4111 1111 1111 1111 |
|
||||
| Kontonummer | `\d{4}.\d{2}.\d{5}` | 1234.56.78901 |
|
||||
| Organisasjonsnr | `\d{9}` | 987654321 |
|
||||
|
||||
---
|
||||
|
||||
## Mutual TLS (mTLS)
|
||||
|
||||
### Klient-sertifikatautentisering
|
||||
|
||||
For AI-API-er med hoyeste sikkerhetskrav, bruk mTLS:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Validate client certificate -->
|
||||
<choose>
|
||||
<when condition="@(context.Request.Certificate == null ||
|
||||
!context.Request.Certificate.Verify() ||
|
||||
context.Request.Certificate.NotAfter < DateTime.UtcNow)">
|
||||
<return-response>
|
||||
<set-status code="403" reason="Forbidden" />
|
||||
<set-body>{"error":{"code":"certificate_required","message":"A valid client certificate is required."}}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
|
||||
<!-- Verify certificate thumbprint against allowed list -->
|
||||
<validate-client-certificate
|
||||
validate-revocation="true"
|
||||
validate-trust="true"
|
||||
validate-not-before="true"
|
||||
validate-not-after="true">
|
||||
<identities>
|
||||
<identity
|
||||
thumbprint="{{AllowedThumbprint1}}"
|
||||
certificate-id="client-cert-app1" />
|
||||
<identity
|
||||
thumbprint="{{AllowedThumbprint2}}"
|
||||
certificate-id="client-cert-app2" />
|
||||
</identities>
|
||||
</validate-client-certificate>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Sertifikatbasert tilgangskontroll per AI-modell
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Map client certificates to model access tiers -->
|
||||
<set-variable name="certSubject"
|
||||
value="@(context.Request.Certificate?.SubjectName?.Name ?? "")" />
|
||||
|
||||
<choose>
|
||||
<!-- Premium tier: Full model access -->
|
||||
<when condition="@(((string)context.Variables["certSubject"]).Contains("OU=Premium"))">
|
||||
<!-- Allow all models -->
|
||||
</when>
|
||||
<!-- Standard tier: Limited models -->
|
||||
<when condition="@(((string)context.Variables["certSubject"]).Contains("OU=Standard"))">
|
||||
<set-variable name="requestedModel"
|
||||
value="@(context.Request.Body.As<JObject>(preserveContent: true)?["model"]?.ToString())" />
|
||||
<choose>
|
||||
<when condition="@(((string)context.Variables["requestedModel"]).Contains("gpt-4") &&
|
||||
!((string)context.Variables["requestedModel"]).Contains("mini"))">
|
||||
<return-response>
|
||||
<set-status code="403" reason="Forbidden" />
|
||||
<set-body>{"error":{"code":"model_not_authorized","message":"Standard tier does not have access to GPT-4o. Use gpt-4o-mini."}}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</when>
|
||||
<otherwise>
|
||||
<return-response>
|
||||
<set-status code="403" reason="Forbidden" />
|
||||
<set-body>{"error":{"code":"certificate_not_authorized","message":"Client certificate not recognized."}}</set-body>
|
||||
</return-response>
|
||||
</otherwise>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Sertifikathondtering med Azure Key Vault
|
||||
|
||||
```bicep
|
||||
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
|
||||
name: keyVaultName
|
||||
}
|
||||
|
||||
resource apimCertificate 'Microsoft.ApiManagement/service/certificates@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'client-root-ca'
|
||||
properties: {
|
||||
keyVault: {
|
||||
secretIdentifier: '${keyVault.properties.vaultUri}secrets/client-root-ca'
|
||||
identityClientId: null // Use system-assigned identity
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Revisjonssporing og audit trail
|
||||
|
||||
### Krav til revisjonssporing
|
||||
|
||||
| Krav | Kilde | APIM-losning |
|
||||
|------|-------|-------------|
|
||||
| Sporbarhet | Forvaltningsloven | Request/response logging med korrelasjons-ID |
|
||||
| Tilgangskontroll | NSM Grunnprinsipper | IP-filter, sertifikat, JWT-validering |
|
||||
| Dataminimering | GDPR Art. 5 | PII-maskering for lagring |
|
||||
| Loggoppbevaring | Arkivloven | Log Analytics retention (90-730 dager) |
|
||||
| Endringssporing | Intern revisjon | APIM audit logs i Activity Log |
|
||||
|
||||
### Omfattende audit trail-policy
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Capture audit context -->
|
||||
<set-variable name="auditContext" value="@{
|
||||
return new JObject {
|
||||
["timestamp"] = DateTime.UtcNow.ToString("o"),
|
||||
["requestId"] = context.RequestId.ToString(),
|
||||
["subscriptionName"] = context.Subscription?.Name,
|
||||
["subscriptionId"] = context.Subscription?.Id,
|
||||
["clientIp"] = context.Request.IpAddress,
|
||||
["userAgent"] = context.Request.Headers.GetValueOrDefault("User-Agent", "unknown"),
|
||||
["apiName"] = context.Api.Name,
|
||||
["apiVersion"] = context.Api.Version,
|
||||
["operationId"] = context.Operation.Id,
|
||||
["certificateSubject"] = context.Request.Certificate?.SubjectName?.Name ?? "none",
|
||||
["tenantId"] = context.Request.Headers.GetValueOrDefault("x-tenant-id", "unknown")
|
||||
}.ToString();
|
||||
}" />
|
||||
</inbound>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Log audit trail -->
|
||||
<trace source="audit-trail" severity="information">
|
||||
<message>@{
|
||||
var audit = JObject.Parse((string)context.Variables["auditContext"]);
|
||||
audit["statusCode"] = context.Response.StatusCode;
|
||||
audit["responseTime"] = (DateTime.UtcNow -
|
||||
DateTime.Parse(audit["timestamp"].ToString())).TotalMilliseconds;
|
||||
|
||||
// Add token usage if available
|
||||
var responseBody = context.Response.Body.As<JObject>(preserveContent: true);
|
||||
if (responseBody?["usage"] != null) {
|
||||
audit["promptTokens"] = responseBody["usage"]["prompt_tokens"];
|
||||
audit["completionTokens"] = responseBody["usage"]["completion_tokens"];
|
||||
audit["totalTokens"] = responseBody["usage"]["total_tokens"];
|
||||
}
|
||||
|
||||
return audit.ToString();
|
||||
}</message>
|
||||
</trace>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### KQL: Sikkerhetsrevisjon
|
||||
|
||||
```kusto
|
||||
// Security audit: Failed authentication attempts
|
||||
ApiManagementGatewayLogs
|
||||
| where TimeGenerated > ago(24h)
|
||||
| where ResponseCode in (401, 403)
|
||||
| summarize
|
||||
FailedAttempts = count(),
|
||||
UniqueIPs = dcount(CallerIpAddress)
|
||||
by CallerIpAddress, ApiId, bin(TimeGenerated, 1h)
|
||||
| where FailedAttempts > 10
|
||||
| order by FailedAttempts desc
|
||||
```
|
||||
|
||||
```kusto
|
||||
// Security audit: Unusual token consumption
|
||||
ApiManagementGatewayLlmLog
|
||||
| where TimeGenerated > ago(24h)
|
||||
| summarize
|
||||
AvgTokens = avg(TotalTokens),
|
||||
MaxTokens = max(TotalTokens),
|
||||
Requests = count()
|
||||
by SubscriptionId
|
||||
| where MaxTokens > 10000 or Requests > 1000
|
||||
| order by MaxTokens desc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sikkerhetssjekksliste for AI Gateway
|
||||
|
||||
| Kontroll | Prioritet | Status |
|
||||
|----------|-----------|--------|
|
||||
| Microsoft Entra ID-autentisering | P0 | |
|
||||
| IP-filtrering (intern/VPN) | P0 | |
|
||||
| Rate limiting (requests og tokens) | P0 | |
|
||||
| Content Safety policy | P0 | |
|
||||
| Prompt injection-deteksjon | P0 | |
|
||||
| TLS 1.2+ patvunget | P0 | |
|
||||
| PII-deteksjon i prompts | P1 | |
|
||||
| Audit trail-logging | P1 | |
|
||||
| mTLS for hoysikkerhet | P1 | |
|
||||
| VNet-integrasjon | P1 | |
|
||||
| Subscription key + JWT | P1 | |
|
||||
| WAF (via Front Door) | P2 | |
|
||||
| DDoS Protection | P2 | |
|
||||
| Private Link | P2 | |
|
||||
| Geo-filtrering | P2 | |
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [AI gateway - Security and safety](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities#security-and-safety) -- AI gateway sikkerhet
|
||||
- [Authenticate and authorize access to AI APIs](https://learn.microsoft.com/en-us/azure/api-management/api-management-authenticate-authorize-ai-apis) -- autentisering
|
||||
- [llm-content-safety policy](https://learn.microsoft.com/en-us/azure/api-management/llm-content-safety-policy) -- innholdssikkerhet
|
||||
- [How to secure APIs using client certificate authentication](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-mutual-certificates-for-clients) -- mTLS
|
||||
- [Restrict caller IPs policy](https://learn.microsoft.com/en-us/azure/api-management/ip-filter-policy) -- IP-filtrering
|
||||
- [Recommendations to mitigate OWASP API Security Top 10](https://learn.microsoft.com/en-us/azure/api-management/mitigate-owasp-api-threats) -- OWASP-anbefalinger
|
||||
- [Secure Azure platform services (PaaS) for AI](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/ai/platform/security) -- Cloud Adoption Framework
|
||||
- [Artificial Intelligence Security benchmark](https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-v2-artificial-intelligence-security) -- AI sikkerhetsbenchmark
|
||||
- [Protect enterprise AI with Prompt Shield](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-ai-prompt-shield) -- Prompt Shields
|
||||
- [Security planning for LLM-based applications](https://learn.microsoft.com/en-us/ai/playbook/technology-guidance/generative-ai/mlops-in-openai/security/security-plan-llm-application) -- sikkerhetsplanlegging
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** nar kunden trenger a herde sin AI gateway for produksjon, oppfylle compliance-krav, eller etablere et forsvar-i-dybden for AI-tjenester.
|
||||
- For norsk offentlig sektor er P0-kontrollene i sjekklisten obligatoriske. Start alltid med Microsoft Entra ID, IP-filtrering, rate limiting og Content Safety -- disse gir den storste sikkerhetseffekten med lavest implementeringskostnad.
|
||||
- PII-filtrering i APIM er en ekstra sikkerhetslinje, men bor ikke vaere eneste tiltak. Anbefal ogsa PII-filtrering i applikasjonslaget og i systemprompt-instruksjoner.
|
||||
- For organisasjoner som behandler sensitiv informasjon (helseopplysninger, personopplysninger), anbefal VNet-integrasjon i Internal mode + mTLS + Azure Private Link som minimumskrav.
|
||||
- Prompt injection-deteksjon i APIM-policies er et forstforsvar, men avanserte angrep krever Azure AI Content Safety med Prompt Shields. Anbefal bade policy-basert og AI-basert deteksjon i lag.
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
# Semantic Caching in APIM
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Semantic caching i Azure API Management er en teknikk som reduserer kostnader og latens for LLM-baserte applikasjoner ved å gjenbruke tidligere genererte completions. I motsetning til tradisjonell nøkkelbasert caching, bruker semantic caching embeddings og vektorlikhet til å identifisere semantisk like prompts -- selv når ordlyden er forskjellig. "Hva er hovedstaden i Norge?" og "Hvilken by er Norges hovedstad?" gir samme cachede svar.
|
||||
|
||||
For norsk offentlig sektor, der mange brukere stiller lignende spørsmål til interne AI-assistenter og chatbots, kan semantic caching gi betydelige kostnadsbesparelser. Typiske kundeservicescenarier med repeterende spørsmål om åpningstider, tjenester og prosedyrer oppnår cache hit rates på 30-60%, noe som tilsvarer tilsvarende reduksjon i token-forbruk og kostnader.
|
||||
|
||||
APIM implementerer semantic caching gjennom dedikerte policies som samarbeider med Azure Managed Redis (med RediSearch-modulen) og en Azure OpenAI Embeddings API-deployment. Hele flyten -- fra prompt-inngang til cache-oppslag og lagring -- håndteres av APIM-policies uten egenutviklet kode.
|
||||
|
||||
---
|
||||
|
||||
## Arkitektur
|
||||
|
||||
### Dataflyt
|
||||
|
||||
```
|
||||
1. Bruker sender prompt til APIM
|
||||
2. APIM → Embeddings API → Vektor [0.23, -0.45, 0.67, ...]
|
||||
3. Vektor → Azure Managed Redis (RediSearch) → Similarity search
|
||||
4. IF similarity score < threshold (lavere = mer lik):
|
||||
RETURN cached completion (cache HIT)
|
||||
ELSE:
|
||||
Forward til Azure OpenAI → Generer completion
|
||||
Store completion + embedding i Redis (cache STORE)
|
||||
RETURN completion til bruker
|
||||
```
|
||||
|
||||
### Komponentoversikt
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Client │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ APIM │
|
||||
│ (AI Gateway)│
|
||||
└──┬───┬──┬──┘
|
||||
│ │ │
|
||||
┌────────┘ │ └────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Embeddings │ │ Redis │ │ Azure │
|
||||
│ API │ │(RediSearch│ │ OpenAI │
|
||||
│ (vektor) │ │ cache) │ │(LLM) │
|
||||
└────────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
| Komponent | Rolle | Azure-tjeneste |
|
||||
|-----------|-------|----------------|
|
||||
| **APIM** | Orkestrerer cache-logikk via policies | Azure API Management (Standard v2+) |
|
||||
| **Embeddings API** | Konverterer prompts til vektorer | Azure OpenAI text-embedding-3-large |
|
||||
| **Vector Cache** | Lagrer embeddings + completions, utfører similarity search | Azure Managed Redis med RediSearch |
|
||||
| **LLM Backend** | Genererer nye completions ved cache miss | Azure OpenAI GPT-4o / GPT-4o-mini |
|
||||
|
||||
---
|
||||
|
||||
## Forutsetninger
|
||||
|
||||
### 1. Azure Managed Redis med RediSearch
|
||||
|
||||
```bicep
|
||||
resource redis 'Microsoft.Cache/redisEnterprise@2024-09-01-preview' = {
|
||||
name: 'redis-semantic-cache-${environment}'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Enterprise_E10'
|
||||
capacity: 2
|
||||
}
|
||||
properties: {}
|
||||
}
|
||||
|
||||
resource database 'Microsoft.Cache/redisEnterprise/databases@2024-09-01-preview' = {
|
||||
parent: redis
|
||||
name: 'default'
|
||||
properties: {
|
||||
clientProtocol: 'Encrypted'
|
||||
evictionPolicy: 'VolatileLRU'
|
||||
modules: [
|
||||
{
|
||||
name: 'RediSearch'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Viktig:** RediSearch-modulen kan KUN aktiveres ved opprettelse av Redis-instansen. Du kan ikke legge den til i etterkant. Planlegg for dette fra starten.
|
||||
|
||||
### 2. Embeddings API Deployment
|
||||
|
||||
```bicep
|
||||
resource embeddingsDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = {
|
||||
parent: openaiAccount
|
||||
name: 'text-embedding-3-large'
|
||||
sku: {
|
||||
name: 'Standard'
|
||||
capacity: 120 // 120K TPM for embeddings
|
||||
}
|
||||
properties: {
|
||||
model: {
|
||||
format: 'OpenAI'
|
||||
name: 'text-embedding-3-large'
|
||||
version: '1'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Valg av embeddings-modell:**
|
||||
|
||||
| Modell | Dimensjoner | Pris (per 1M tokens) | Anbefaling |
|
||||
|--------|-------------|---------------------|------------|
|
||||
| text-embedding-3-large | 3072 | ~$0.13 | Høyest kvalitet, anbefalt |
|
||||
| text-embedding-3-small | 1536 | ~$0.02 | Kostnadseffektiv, god nok for de fleste |
|
||||
| text-embedding-ada-002 | 1536 | ~$0.10 | Legacy, ikke anbefalt for nye prosjekter |
|
||||
|
||||
### 3. APIM External Cache-konfigurasjon
|
||||
|
||||
Koble Redis som ekstern cache i APIM:
|
||||
|
||||
```bicep
|
||||
resource externalCache 'Microsoft.ApiManagement/service/caches@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'redis-semantic'
|
||||
properties: {
|
||||
connectionString: '${redis.properties.hostName}:10000,password=${listKeys(redis.id, redis.apiVersion).keys[0].value},ssl=True,abortConnect=False'
|
||||
useFromLocation: 'default'
|
||||
description: 'Azure Managed Redis for semantic caching'
|
||||
resourceId: redis.id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Embeddings Backend i APIM
|
||||
|
||||
```bicep
|
||||
resource embeddingsBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
|
||||
parent: apim
|
||||
name: 'embeddings-backend'
|
||||
properties: {
|
||||
url: 'https://aoai-norwayeast.openai.azure.com/openai/deployments/text-embedding-3-large/embeddings'
|
||||
protocol: 'http'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache-lookup og Cache-store Policies
|
||||
|
||||
### Azure OpenAI-spesifikke policies
|
||||
|
||||
For Azure OpenAI APIs, bruk de spesialbestemte policies:
|
||||
|
||||
**Inbound (cache lookup):**
|
||||
|
||||
```xml
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
ignore-system-messages="true"
|
||||
max-message-count="10">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
```
|
||||
|
||||
**Outbound (cache store):**
|
||||
|
||||
```xml
|
||||
<azure-openai-semantic-cache-store duration="3600" />
|
||||
```
|
||||
|
||||
### Generelle LLM-policies
|
||||
|
||||
For tredjeparts LLM-er eller OpenAI-kompatible endepunkter:
|
||||
|
||||
**Inbound:**
|
||||
|
||||
```xml
|
||||
<llm-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
max-message-count="10">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</llm-semantic-cache-lookup>
|
||||
```
|
||||
|
||||
**Outbound:**
|
||||
|
||||
```xml
|
||||
<llm-semantic-cache-store duration="3600" />
|
||||
```
|
||||
|
||||
### Policy-attributter
|
||||
|
||||
| Attributt | Type | Beskrivelse | Anbefalt verdi |
|
||||
|-----------|------|-------------|----------------|
|
||||
| `score-threshold` | float | Maks avstand for cache hit (lavere = strengere) | 0.10-0.20 |
|
||||
| `embeddings-backend-id` | string | Backend-ID for Embeddings API | `embeddings-backend` |
|
||||
| `embeddings-backend-auth` | string | Autentiseringsmetode | `system-assigned` |
|
||||
| `ignore-system-messages` | bool | Ignorer system message i cache-nøkkel | `true` (oftest) |
|
||||
| `max-message-count` | int | Maks antall meldinger i konversasjonshistorikk å cache | 10 |
|
||||
| `duration` | int | Cache TTL i sekunder | 3600 (1 time) |
|
||||
|
||||
### vary-by-element
|
||||
|
||||
`<vary-by>` sikrer at cache er isolert per konsument/kontekst:
|
||||
|
||||
```xml
|
||||
<!-- Isoler cache per subscription -->
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
|
||||
<!-- Isoler per subscription OG modell -->
|
||||
<vary-by>@(context.Subscription.Id + "-" + context.Request.MatchedParameters["deployment-id"])</vary-by>
|
||||
|
||||
<!-- Isoler per etat -->
|
||||
<vary-by>@(context.Request.Headers.GetValueOrDefault("x-etat-id", "shared"))</vary-by>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedding-Based Similarity
|
||||
|
||||
### Hvordan score-threshold fungerer
|
||||
|
||||
APIM bruker cosine distance (ikke cosine similarity) for å sammenligne embeddings:
|
||||
|
||||
```
|
||||
Cosine Distance = 1 - Cosine Similarity
|
||||
|
||||
Distance 0.0 = Identiske prompts (perfekt match)
|
||||
Distance 0.15 = Svært like prompts
|
||||
Distance 0.30 = Noe like prompts
|
||||
Distance 1.0 = Helt ulike prompts
|
||||
```
|
||||
|
||||
**Threshold-valg:**
|
||||
|
||||
| score-threshold | Matchstrenghet | Cache hit rate | Presisjon | Anbefalt for |
|
||||
|-----------------|---------------|----------------|-----------|--------------|
|
||||
| 0.05 | Ekstremt streng | Lav (5-15%) | Svært høy | Faktabaserte spørsmål |
|
||||
| 0.10 | Streng | Moderat (15-30%) | Høy | Standard anbefaling |
|
||||
| 0.15 | Balansert | God (25-45%) | God | De fleste use cases |
|
||||
| 0.20 | Liberal | Høy (35-60%) | Moderat | FAQ/kundeservice |
|
||||
| 0.30 | Aggressiv | Svært høy (50-70%) | Lavere | Generelle spørsmål |
|
||||
|
||||
**Kalibreringsprosess:**
|
||||
|
||||
1. Start med `score-threshold="0.15"` (balansert)
|
||||
2. Kjør produksjonstrafikk i 1-2 uker
|
||||
3. Analyser cache hit rate og brukertilfredshet
|
||||
4. Juster ned (strengere) hvis brukere rapporterer irrelevante svar
|
||||
5. Juster opp (mer liberal) hvis cache hit rate er under 20%
|
||||
|
||||
### Eksempler på semantisk matching
|
||||
|
||||
| Prompt A | Prompt B | Typisk distance | Match ved 0.15? |
|
||||
|----------|----------|-----------------|------------------|
|
||||
| "Hva er hovedstaden i Norge?" | "Hvilken by er Norges hovedstad?" | ~0.05 | Ja |
|
||||
| "Forklar maskinlæring" | "Hva er machine learning?" | ~0.10 | Ja |
|
||||
| "Hvordan søker jeg om byggetillatelse?" | "Prosessen for å få byggetillatelse" | ~0.12 | Ja |
|
||||
| "Hva er veibygging?" | "Hvordan bygger man en bro?" | ~0.25 | Nei |
|
||||
| "Fortell om AI" | "Hva er kvantemekanikk?" | ~0.45 | Nei |
|
||||
|
||||
---
|
||||
|
||||
## Cache Invalidation Strategies
|
||||
|
||||
### TTL-basert invalidation (standard)
|
||||
|
||||
```xml
|
||||
<!-- Cache entries utløper etter 1 time -->
|
||||
<azure-openai-semantic-cache-store duration="3600" />
|
||||
```
|
||||
|
||||
**Anbefalte TTL-verdier:**
|
||||
|
||||
| Innholdstype | TTL | Begrunnelse |
|
||||
|-------------|-----|-------------|
|
||||
| Statisk fakta (hovedsteder, lover) | 86400 (24t) | Endres sjelden |
|
||||
| Generell kunnskap | 3600 (1t) | God balanse |
|
||||
| Dynamisk innhold (priser, status) | 300 (5min) | Endres ofte |
|
||||
| Real-time data | 0 (ingen cache) | Må alltid være oppdatert |
|
||||
|
||||
### Manuell cache-invalidation
|
||||
|
||||
APIM har ingen innebygd policy for selektiv cache-invalidation av semantic cache. Alternativa tilnærminger:
|
||||
|
||||
**1. Redis CLI flush:**
|
||||
```bash
|
||||
# Flush all cached entries (krever Redis-tilgang)
|
||||
redis-cli -h redis-cache.norwayeast.redis.cache.windows.net -p 10000 --tls FLUSHDB
|
||||
```
|
||||
|
||||
**2. TTL-basert rotasjon:**
|
||||
Bruk kort TTL og la entries utløpe naturlig.
|
||||
|
||||
**3. vary-by med versjonsnøkkel:**
|
||||
```xml
|
||||
<vary-by>@("v2-" + context.Subscription.Id)</vary-by>
|
||||
```
|
||||
Endre "v2" til "v3" i policy for å effektivt invalidere all cache (nye nøkler gir cache miss).
|
||||
|
||||
---
|
||||
|
||||
## Cost Savings Analysis
|
||||
|
||||
### Beregningsmodell
|
||||
|
||||
```
|
||||
Kostnad UTEN caching:
|
||||
Totale requests × gjennomsnittlig tokens per request × pris per token
|
||||
|
||||
Kostnad MED caching:
|
||||
(Cache misses × tokens per request × pris per token)
|
||||
+ (Alle requests × embedding tokens × embedding pris)
|
||||
+ Redis-kostnad
|
||||
|
||||
Besparelse = Kostnad UTEN - Kostnad MED
|
||||
```
|
||||
|
||||
### Eksempelberegning for norsk offentlig sektor
|
||||
|
||||
**Scenario:** Intern AI-assistent for 500 ansatte, 10 000 requests/dag.
|
||||
|
||||
| Parameter | Verdi |
|
||||
|-----------|-------|
|
||||
| Requests per dag | 10 000 |
|
||||
| Gjennomsnittlig prompt tokens | 200 |
|
||||
| Gjennomsnittlig completion tokens | 500 |
|
||||
| GPT-4o pris (input) | $2.50 / 1M tokens |
|
||||
| GPT-4o pris (output) | $10.00 / 1M tokens |
|
||||
| Embedding pris | $0.13 / 1M tokens |
|
||||
| Cache hit rate | 40% |
|
||||
|
||||
**Beregning:**
|
||||
|
||||
| Kostnadspost | Uten caching | Med caching (40% hit rate) |
|
||||
|-------------|-------------|---------------------------|
|
||||
| LLM input tokens | 10K × 200 = 2M → $5.00/dag | 6K × 200 = 1.2M → $3.00/dag |
|
||||
| LLM output tokens | 10K × 500 = 5M → $50.00/dag | 6K × 500 = 3M → $30.00/dag |
|
||||
| Embedding tokens | $0/dag | 10K × 200 = 2M → $0.26/dag |
|
||||
| Redis (E10) | $0/dag | ~$6.00/dag ($182/mnd) |
|
||||
| **Total per dag** | **$55.00** | **$39.26** |
|
||||
| **Total per måned** | **$1 650** | **$1 178** |
|
||||
| **Besparelse** | - | **$472/mnd (29%)** |
|
||||
|
||||
### ROI-beregning
|
||||
|
||||
| Cache hit rate | Månedlig besparelse (LLM) | Redis-kostnad | Netto besparelse | ROI |
|
||||
|----------------|--------------------------|---------------|-----------------|-----|
|
||||
| 20% | $330 | $182 | $148 | Positiv |
|
||||
| 40% | $660 | $182 | $478 | Sterk |
|
||||
| 60% | $990 | $182 | $808 | Svært sterk |
|
||||
| 80% | $1 320 | $182 | $1 138 | Eksepsjonell |
|
||||
|
||||
> **Break-even punkt:** Semantic caching er kostnadseffektivt ved cache hit rates over ~15% for typiske workloads.
|
||||
|
||||
---
|
||||
|
||||
## Privacy Considerations
|
||||
|
||||
### Datalagrings-hensyn
|
||||
|
||||
| Hensyn | Risiko | Mitigering |
|
||||
|--------|--------|------------|
|
||||
| **PII i cache** | Persondata caches i Redis | Bruk `vary-by` per bruker, kort TTL, eller ekskluder PII-requests |
|
||||
| **Cross-tenant data** | En brukers svar vises for annen bruker | `vary-by` per subscription/bruker isolerer cache |
|
||||
| **Cache i feil region** | Data lagres utenfor tillatt geografi | Deploy Redis i samme region som APIM og OpenAI |
|
||||
| **Langvarig lagring** | Sensitive svar lagret for lenge | Sett passende TTL, minimum mulig |
|
||||
| **Logging av prompts** | Prompts logges via APIM diagnostics | Konfigurer masking i diagnostic settings |
|
||||
|
||||
### Anbefalinger for offentlig sektor
|
||||
|
||||
1. **Isoler cache per etat/avdeling** med `vary-by` element
|
||||
2. **Sett TTL til maksimalt 1 time** for generelle spørsmål, kortere for sensitive
|
||||
3. **Ekskluder sensitive APIer** fra semantic caching (fjern policies for spesifikke operasjoner)
|
||||
4. **Deploy Redis i Norway East** eller Sweden Central for datasuverenitet
|
||||
5. **Aktiver TLS** (`ssl=True`) for all Redis-kommunikasjon
|
||||
6. **Bruk private endpoints** for Redis og APIM
|
||||
7. **Vurder DPIA** (Data Protection Impact Assessment) for cache av brukerdata
|
||||
|
||||
### Ekskludering av sensitive requests
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- Skip cache for requests med PII-flag -->
|
||||
<choose>
|
||||
<when condition="@(context.Request.Headers.GetValueOrDefault("x-contains-pii", "false") == "true")">
|
||||
<!-- Ingen cache lookup, gå direkte til backend -->
|
||||
</when>
|
||||
<otherwise>
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
</otherwise>
|
||||
</choose>
|
||||
</inbound>
|
||||
|
||||
<outbound>
|
||||
<choose>
|
||||
<when condition="@(context.Request.Headers.GetValueOrDefault("x-contains-pii", "false") != "true")">
|
||||
<azure-openai-semantic-cache-store duration="3600" />
|
||||
</when>
|
||||
</choose>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting etter Cache Lookup
|
||||
|
||||
### Beskyttelse mot cache-utilgjengelighet
|
||||
|
||||
Legg alltid til en rate limit ETTER cache lookup for å beskytte backend hvis Redis er nede:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- 1. Semantic cache lookup -->
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
|
||||
<!-- 2. Rate limit for cache misses (beskytter backend) -->
|
||||
<rate-limit-by-key
|
||||
calls="100"
|
||||
renewal-period="60"
|
||||
counter-key="@(context.Subscription.Id)" />
|
||||
|
||||
<!-- 3. Token limit -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
estimate-prompt-tokens="true" />
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verifisering og feilsøking
|
||||
|
||||
### Bekrefte at caching fungerer
|
||||
|
||||
Bruk APIM Test Console med tracing aktivert:
|
||||
|
||||
1. Send en request via Test Console med tracing
|
||||
2. Inspiser trace-output:
|
||||
- **Cache HIT:** `azure-openai-semantic-cache-lookup` viser "Cache lookup resulted in a hit"
|
||||
- **Cache MISS:** Viser "Cache lookup resulted in a miss" + backend-kall
|
||||
|
||||
### KQL for cache-metrikk
|
||||
|
||||
```kusto
|
||||
// Cache hit rate over tid
|
||||
ApiManagementGatewayLogs
|
||||
| where OperationId contains "chat"
|
||||
| extend cacheHit = ResponseHeaders contains "x-cache: HIT"
|
||||
| summarize
|
||||
TotalRequests = count(),
|
||||
CacheHits = countif(cacheHit),
|
||||
CacheMisses = countif(not(cacheHit)),
|
||||
HitRate = round(100.0 * countif(cacheHit) / count(), 2)
|
||||
by bin(TimeGenerated, 1h)
|
||||
| render timechart
|
||||
```
|
||||
|
||||
```kusto
|
||||
// Kostnadsbesparelse estimat
|
||||
ApiManagementGatewayLogs
|
||||
| where OperationId contains "chat"
|
||||
| extend cacheHit = ResponseHeaders contains "x-cache: HIT"
|
||||
| extend estimatedTokensSaved = iff(cacheHit, 700, 0) // avg tokens per request
|
||||
| summarize
|
||||
TokensSaved = sum(estimatedTokensSaved),
|
||||
EstimatedCostSavedUSD = round(sum(estimatedTokensSaved) * 0.000010, 2)
|
||||
by bin(TimeGenerated, 1d)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Komplett policy for semantic caching
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- Autentisering -->
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com/" />
|
||||
|
||||
<!-- Token rate limit -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
estimate-prompt-tokens="true"
|
||||
remaining-tokens-variable-name="remainingTokens" />
|
||||
|
||||
<!-- Semantic cache lookup -->
|
||||
<azure-openai-semantic-cache-lookup
|
||||
score-threshold="0.15"
|
||||
embeddings-backend-id="embeddings-backend"
|
||||
embeddings-backend-auth="system-assigned"
|
||||
ignore-system-messages="true"
|
||||
max-message-count="10">
|
||||
<vary-by>@(context.Subscription.Id)</vary-by>
|
||||
</azure-openai-semantic-cache-lookup>
|
||||
|
||||
<!-- Fallback rate limit hvis cache er nede -->
|
||||
<rate-limit-by-key
|
||||
calls="100"
|
||||
renewal-period="60"
|
||||
counter-key="@(context.Subscription.Id)" />
|
||||
|
||||
<!-- Backend pool -->
|
||||
<set-backend-service backend-id="openai-pool" />
|
||||
</inbound>
|
||||
|
||||
<outbound>
|
||||
<base />
|
||||
|
||||
<!-- Cache store -->
|
||||
<azure-openai-semantic-cache-store duration="3600" />
|
||||
|
||||
<!-- Token metrikk -->
|
||||
<llm-emit-token-metric namespace="ai-gateway">
|
||||
<dimension name="Subscription"
|
||||
value="@(context.Subscription.Id)" />
|
||||
<dimension name="CacheHit"
|
||||
value="@(context.Response.Headers.GetValueOrDefault("x-cache", "MISS"))" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tier-kompatibilitet
|
||||
|
||||
| Policy | Classic | V2 | Consumption | Self-hosted | Workspace |
|
||||
|--------|---------|----|-----------|----------- |-----------|
|
||||
| `azure-openai-semantic-cache-lookup` | Ja | Ja | Ja | Nei | Nei |
|
||||
| `azure-openai-semantic-cache-store` | Ja | Ja | Ja | Nei | Nei |
|
||||
| `llm-semantic-cache-lookup` | Ja | Ja | Ja | Nei | Nei |
|
||||
| `llm-semantic-cache-store` | Ja | Ja | Ja | Nei | Nei |
|
||||
|
||||
> **Merk:** Semantic caching krever ekstern cache (Azure Managed Redis) og er IKKE tilgjengelig i self-hosted gateway eller workspace gateway.
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- Semantic caching er den mest kostnadseffektive optimaliseringen for AI-workloads med repeterende spørsmål. Start med `score-threshold="0.15"` og juster basert på cache hit rate og brukerfeedback. For FAQ/kundeservice-scenarier, vurder 0.20 for høyere hit rate.
|
||||
- Krav: Azure Managed Redis med RediSearch-modul (MÅ aktiveres ved opprettelse, kan ikke legges til etterpå) + Azure OpenAI Embeddings deployment. Planlegg disse ressursene fra starten.
|
||||
- Bruk `vary-by` per subscription/bruker for å isolere cache og forhindre data-lekkasje mellom konsumenter. For offentlig sektor er dette en forutsetning for compliance.
|
||||
- Legg alltid til en `rate-limit` policy ETTER cache lookup som beskyttelse mot situasjoner der Redis er utilgjengelig -- uten dette vil alle requests gå direkte til backend uten throttling.
|
||||
- Kostnadsbesparelse ved 40% cache hit rate er typisk 25-35% for standard AI-assistenter. Break-even punkt er ca. 15% hit rate (under dette er Redis-kostnaden høyere enn besparelsen).
|
||||
|
|
@ -0,0 +1,520 @@
|
|||
# Streaming Support in APIM for AI Responses
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Streaming av AI-responser er en nøkkelfunksjon for å levere god brukeropplevelse i chat-applikasjoner. Azure OpenAI støtter Server-Sent Events (SSE) for å streame chat completions token-for-token til klienten, noe som gir umiddelbar feedback i stedet for å vente på en komplett respons. Når Azure API Management (APIM) sitter mellom klient og Azure OpenAI, krever denne streaming-arkitekturen spesifikk konfigurasjon for å fungere korrekt.
|
||||
|
||||
For norsk offentlig sektor som bygger AI-chatboter og assistenter er streaming kritisk for brukeropplevelsen. Uten streaming kan brukere vente 10-30 sekunder på svar fra store modeller som GPT-4o — med streaming begynner svar å vises innen 1-2 sekunder. Denne referansen dekker alle aspekter ved konfigurering av APIM for streaming av AI-responser, inkludert SSE forwarding, buffering-policyer, timeout-håndtering og klientkompatibilitet.
|
||||
|
||||
APIM støtter SSE gjennom klassiske og v2-tiers (ikke Consumption-tier). Korrekt konfigurasjon krever at flere aspekter justeres: response buffering må deaktiveres, timeouts må økes, og logging-konfigurasjonen må tilpasses for å unngå at streaming-responser bufres opp.
|
||||
|
||||
---
|
||||
|
||||
## SSE Forwarding
|
||||
|
||||
### Slik Fungerer SSE med Azure OpenAI
|
||||
|
||||
Når `"stream": true` settes i chat completion-forespørselen, returnerer Azure OpenAI en strøm av Server-Sent Events:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/event-stream
|
||||
Transfer-Encoding: chunked
|
||||
Connection: keep-alive
|
||||
|
||||
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
|
||||
|
||||
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hei"},"index":0}]}
|
||||
|
||||
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":" på"},"index":0}]}
|
||||
|
||||
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":" deg"},"index":0}]}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
### APIM som SSE Proxy
|
||||
|
||||
APIM fungerer som en transparent proxy for SSE-trafikk mellom klient og Azure OpenAI:
|
||||
|
||||
```
|
||||
Klient → APIM Gateway → Azure OpenAI
|
||||
(SSE proxy) (SSE source)
|
||||
|
||||
1. Klient sender POST med "stream": true
|
||||
2. APIM forwarder til Azure OpenAI
|
||||
3. Azure OpenAI begynner å streame SSE-data
|
||||
4. APIM relayer hvert SSE-event umiddelbart til klient
|
||||
5. Azure OpenAI sender "data: [DONE]"
|
||||
6. Forbindelsen lukkes
|
||||
```
|
||||
|
||||
### Krav for SSE Forwarding
|
||||
|
||||
| Krav | Innstilling | Merknader |
|
||||
|------|------------|-----------|
|
||||
| APIM Tier | Classic eller v2 | Consumption-tier støttes IKKE |
|
||||
| Response buffering | Deaktivert | `buffer-response="false"` |
|
||||
| Keepalive | Aktivert | Unngå 4 min idle timeout |
|
||||
| Response body logging | Deaktivert | Unngår buffering |
|
||||
| Caching | Deaktivert | For SSE-endepunkter |
|
||||
|
||||
---
|
||||
|
||||
## Buffering Policies
|
||||
|
||||
### Deaktivere Response Buffering
|
||||
|
||||
Den viktigste konfigurasjonen for streaming er å deaktivere response buffering i `forward-request`:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Standard APIM inbound-policyer (autentisering, rate limiting, etc.) -->
|
||||
<set-backend-service backend-id="aoai-backend" />
|
||||
</inbound>
|
||||
<backend>
|
||||
<!-- KRITISK: buffer-response="false" for streaming -->
|
||||
<forward-request timeout="120"
|
||||
fail-on-error-status-code="true"
|
||||
buffer-response="false" />
|
||||
</backend>
|
||||
<outbound>
|
||||
<base />
|
||||
</outbound>
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Policyer som MÅ Unngås med Streaming
|
||||
|
||||
Følgende policyer buffrer responsen og er IKKE kompatible med SSE:
|
||||
|
||||
| Policy | Problem | Alternativ |
|
||||
|--------|---------|-----------|
|
||||
| `validate-content` | Buffrer full respons for validering | Valider kun inbound request |
|
||||
| `xml-to-json` / `json-to-xml` | Trenger full respons for konvertering | Ikke aktuelt for SSE |
|
||||
| `xslt-transform` | Buffrer for transformasjon | Ikke aktuelt for SSE |
|
||||
| `cache-store` | Lagrer full respons | Bruk `llm-semantic-cache-store` |
|
||||
| `log-to-eventhub` (med body) | Buffrer respons for logging | Logg kun headers |
|
||||
|
||||
### Betinget Buffering
|
||||
|
||||
Aktiver buffering kun for ikke-streaming requests:
|
||||
|
||||
```xml
|
||||
<backend>
|
||||
<choose>
|
||||
<!-- Sjekk om request er streaming -->
|
||||
<when condition="@{
|
||||
var body = context.Request.Body.As<JObject>(preserveContent: true);
|
||||
return body != null && body["stream"]?.Value<bool>() == true;
|
||||
}">
|
||||
<!-- Streaming: IKKE buffer -->
|
||||
<forward-request timeout="240"
|
||||
fail-on-error-status-code="true"
|
||||
buffer-response="false" />
|
||||
</when>
|
||||
<otherwise>
|
||||
<!-- Ikke-streaming: buffer er OK -->
|
||||
<forward-request timeout="120"
|
||||
fail-on-error-status-code="true"
|
||||
buffer-response="true" />
|
||||
</otherwise>
|
||||
</choose>
|
||||
</backend>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunked Responses
|
||||
|
||||
### Transfer-Encoding: chunked
|
||||
|
||||
SSE-responses fra Azure OpenAI bruker chunked transfer encoding. APIM håndterer dette automatisk når `buffer-response="false"`:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/event-stream
|
||||
Transfer-Encoding: chunked
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
```
|
||||
|
||||
### Response Headers for Korrekt Streaming
|
||||
|
||||
Backend-tjenesten (Azure OpenAI) sender disse headerne:
|
||||
|
||||
| Header | Verdi | Formål |
|
||||
|--------|-------|--------|
|
||||
| `Content-Type` | `text/event-stream` | Signaliserer SSE til klient |
|
||||
| `Transfer-Encoding` | `chunked` | Tillater streaming uten Content-Length |
|
||||
| `Connection` | `keep-alive` | Holder TCP-forbindelsen åpen |
|
||||
| `Cache-Control` | `no-cache` | Forhindrer mellomlagring |
|
||||
|
||||
### APIM Policy for Response Headers
|
||||
|
||||
Sørg for at APIM ikke overstyrer kritiske streaming-headers:
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Sørg for at streaming-headers videresendes korrekt -->
|
||||
<choose>
|
||||
<when condition="@(context.Response.Headers.GetValueOrDefault("Content-Type","").Contains("text/event-stream"))">
|
||||
<!-- Ikke legg til Cache-Control som kan interferere -->
|
||||
<set-header name="X-Stream-Response" exists-action="override">
|
||||
<value>true</value>
|
||||
</set-header>
|
||||
</when>
|
||||
</choose>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timeout Management for Streams
|
||||
|
||||
### Idle Connection Timeout
|
||||
|
||||
Azure Load Balancer (som brukes i APIM-infrastrukturen) har en standard idle timeout på 4 minutter. For streaming-scenarier der det kan gå tid mellom tokens:
|
||||
|
||||
```
|
||||
Strategi 1: Backend keepalive
|
||||
→ Azure OpenAI sender SSE-events fortløpende
|
||||
→ Normalt ikke et problem med aktiv streaming
|
||||
|
||||
Strategi 2: Klient keepalive
|
||||
→ Klient sender "ping" minst hvert 4. minutt
|
||||
→ Aktuelt for langvarige idle-forbindelser
|
||||
|
||||
Strategi 3: Økt timeout via policy
|
||||
→ forward-request timeout="240"
|
||||
→ Dekker de fleste scenarier
|
||||
```
|
||||
|
||||
### Timeout-verdier for Streaming
|
||||
|
||||
| Parameter | Standard | Anbefalt for streaming | Merknader |
|
||||
|-----------|---------|----------------------|-----------|
|
||||
| `forward-request timeout` | 300 sek | 120-240 sek | Avhenger av maks respons-lengde |
|
||||
| Azure LB idle timeout | 240 sek | Ikke konfigurerbar i APIM | Bruk keepalive |
|
||||
| DNS TTL | Varierer | N/A | Påvirker failover |
|
||||
|
||||
### Timeout Policy for Streaming Endpoints
|
||||
|
||||
```xml
|
||||
<backend>
|
||||
<forward-request timeout="240"
|
||||
fail-on-error-status-code="true"
|
||||
buffer-response="false" />
|
||||
</backend>
|
||||
|
||||
<on-error>
|
||||
<base />
|
||||
<choose>
|
||||
<when condition="@(context.LastError.Source == "forward-request" &&
|
||||
context.LastError.Reason == "Timeout")">
|
||||
<return-response>
|
||||
<set-status code="504" reason="Gateway Timeout" />
|
||||
<set-body>{
|
||||
"error": {
|
||||
"code": "StreamingTimeout",
|
||||
"message": "The AI model did not complete its response within the timeout period."
|
||||
}
|
||||
}</set-body>
|
||||
</return-response>
|
||||
</when>
|
||||
</choose>
|
||||
</on-error>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client Compatibility
|
||||
|
||||
### JavaScript/TypeScript EventSource
|
||||
|
||||
```typescript
|
||||
// Standard EventSource for SSE
|
||||
const response = await fetch('/api/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Ocp-Apim-Subscription-Key': subscriptionKey
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: [{ role: 'user', content: 'Hei, Cosmo!' }],
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
|
||||
|
||||
for (const line of lines) {
|
||||
const data = line.slice(6); // Fjern "data: " prefiks
|
||||
if (data === '[DONE]') break;
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
const content = parsed.choices[0]?.delta?.content;
|
||||
if (content) {
|
||||
process.stdout.write(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python med httpx
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import json
|
||||
|
||||
async def stream_completion(prompt: str):
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with client.stream(
|
||||
"POST",
|
||||
f"{APIM_ENDPOINT}/openai/deployments/gpt-4o/chat/completions",
|
||||
params={"api-version": "2024-10-21"},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY,
|
||||
},
|
||||
json={
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": True
|
||||
},
|
||||
timeout=120.0
|
||||
) as response:
|
||||
async for line in response.aiter_lines():
|
||||
if line.startswith("data: "):
|
||||
data = line[6:]
|
||||
if data == "[DONE]":
|
||||
break
|
||||
chunk = json.loads(data)
|
||||
content = chunk["choices"][0]["delta"].get("content", "")
|
||||
print(content, end="", flush=True)
|
||||
```
|
||||
|
||||
### C# med Azure.AI.OpenAI
|
||||
|
||||
```csharp
|
||||
var client = new AzureOpenAIClient(
|
||||
new Uri(apimEndpoint),
|
||||
new AzureKeyCredential(subscriptionKey));
|
||||
|
||||
var chatClient = client.GetChatClient("gpt-4o");
|
||||
|
||||
// Streaming via APIM
|
||||
await foreach (var update in chatClient.CompleteChatStreamingAsync(
|
||||
new ChatMessage[] { new UserChatMessage("Hei, Cosmo!") }))
|
||||
{
|
||||
foreach (var part in update.ContentUpdate)
|
||||
{
|
||||
Console.Write(part.Text);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Klientkrav for APIM-proxy
|
||||
|
||||
| Krav | Beskrivelse |
|
||||
|------|-------------|
|
||||
| Subscription key | `Ocp-Apim-Subscription-Key` header eller query parameter |
|
||||
| Timeout | Minst 120 sekunder for streaming |
|
||||
| Chunked decoding | Håndtere `Transfer-Encoding: chunked` |
|
||||
| SSE parsing | Parse `data: ` prefiks og `[DONE]` sentinel |
|
||||
| Connection handling | Håndtere mid-stream connection drops gracefully |
|
||||
|
||||
---
|
||||
|
||||
## Logging av Streaming-requests
|
||||
|
||||
### Utfordringer med Streaming-logging
|
||||
|
||||
Når response body logges, bufres hele responsen — noe som bryter streaming. Korrekt logging for SSE-endepunkter:
|
||||
|
||||
```xml
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Logg inbound request (prompt) — dette er OK -->
|
||||
<log-to-eventhub logger-id="ai-eventhub-logger">
|
||||
@{
|
||||
var body = context.Request.Body.As<string>(preserveContent: true);
|
||||
return new JObject(
|
||||
new JProperty("timestamp", DateTime.UtcNow),
|
||||
new JProperty("method", context.Request.Method),
|
||||
new JProperty("url", context.Request.Url.ToString()),
|
||||
new JProperty("prompt", body)
|
||||
).ToString();
|
||||
}
|
||||
</log-to-eventhub>
|
||||
</inbound>
|
||||
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- IKKE logg response body for streaming — det bufrer responsen -->
|
||||
<!-- Logg kun metadata -->
|
||||
<choose>
|
||||
<when condition="@(!context.Response.Headers.GetValueOrDefault("Content-Type","").Contains("text/event-stream"))">
|
||||
<!-- Kun for ikke-streaming responses -->
|
||||
<log-to-eventhub logger-id="ai-eventhub-logger">
|
||||
@{
|
||||
return new JObject(
|
||||
new JProperty("statusCode", context.Response.StatusCode),
|
||||
new JProperty("responseBody", context.Response.Body.As<string>(preserveContent: true))
|
||||
).ToString();
|
||||
}
|
||||
</log-to-eventhub>
|
||||
</when>
|
||||
</choose>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### APIM Diagnostic Settings for Streaming
|
||||
|
||||
Deaktiver response body logging for APIs som bruker streaming:
|
||||
|
||||
```
|
||||
1. Naviger til API → Settings → Diagnostic Logs
|
||||
2. Azure Monitor-fanen:
|
||||
- Frontend Response: Body bytes = 0
|
||||
- Backend Response: Body bytes = 0
|
||||
3. Application Insights-fanen:
|
||||
- Body bytes to log: 0 (for streaming APIs)
|
||||
```
|
||||
|
||||
### LLM API Logging (Azure Monitor)
|
||||
|
||||
For APIM sin innebygde LLM-logging:
|
||||
|
||||
```
|
||||
1. APIM → Monitoring → Diagnostic settings
|
||||
2. Velg "Logs related to generative AI gateway"
|
||||
3. Send to Log Analytics workspace
|
||||
4. NB: Log LLM messages fungerer kun for IKKE-streaming requests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token-telling for Streaming
|
||||
|
||||
### Utfordring
|
||||
|
||||
Ved streaming returnerer Azure OpenAI token-bruk i siste chunk (`usage` feltet). APIM sin `llm-emit-token-metric` policy krever tilgang til dette:
|
||||
|
||||
```json
|
||||
// Siste chunk i streaming-respons
|
||||
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk",
|
||||
"choices":[{"delta":{},"index":0,"finish_reason":"stop"}],
|
||||
"usage":{"prompt_tokens":15,"completion_tokens":42,"total_tokens":57}}
|
||||
```
|
||||
|
||||
### Policy for Token-metriker (Ikke-streaming)
|
||||
|
||||
For ikke-streaming requests, bruk standard `llm-emit-token-metric` i outbound:
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<base />
|
||||
<llm-emit-token-metric namespace="ai-metrics">
|
||||
<dimension name="API" value="@(context.Api.Name)" />
|
||||
<dimension name="User" value="@(context.Subscription.Name)" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
**Merk:** `llm-emit-token-metric` fungerer for både streaming og ikke-streaming requests. APIM håndterer parsing av streaming-chunks for å ekstrahere token-bruk automatisk.
|
||||
|
||||
---
|
||||
|
||||
## Komplett Streaming-policy
|
||||
|
||||
### Full Policy for Streaming AI Gateway
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
|
||||
<!-- Autentisering -->
|
||||
<validate-azure-ad-token tenant-id="{{TENANT_ID}}"
|
||||
header-name="Authorization" />
|
||||
|
||||
<!-- Token rate limiting -->
|
||||
<llm-token-limit counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="10000"
|
||||
estimate-prompt-tokens="true" />
|
||||
|
||||
<!-- Backend med managed identity -->
|
||||
<set-backend-service backend-id="aoai-pool" />
|
||||
<authentication-managed-identity
|
||||
resource="https://cognitiveservices.azure.com"
|
||||
output-token-variable-name="mi-token" />
|
||||
<set-header name="Authorization" exists-action="override">
|
||||
<value>@("Bearer " + (string)context.Variables["mi-token"])</value>
|
||||
</set-header>
|
||||
</inbound>
|
||||
|
||||
<backend>
|
||||
<!-- Streaming-kompatibel forwarding -->
|
||||
<forward-request timeout="240"
|
||||
fail-on-error-status-code="true"
|
||||
buffer-response="false" />
|
||||
</backend>
|
||||
|
||||
<outbound>
|
||||
<base />
|
||||
|
||||
<!-- Token-metriker (fungerer for streaming og ikke-streaming) -->
|
||||
<llm-emit-token-metric namespace="ai-metrics">
|
||||
<dimension name="Subscription" value="@(context.Subscription.Name)" />
|
||||
<dimension name="API" value="@(context.Api.Name)" />
|
||||
<dimension name="Region" value="@(context.Deployment.Region)" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
|
||||
<on-error>
|
||||
<base />
|
||||
</on-error>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Configure API for server-sent events](https://learn.microsoft.com/en-us/azure/api-management/how-to-server-sent-events) — Offisiell SSE-guide for APIM
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) — AI gateway oversikt
|
||||
- [Azure OpenAI REST API reference - Chat Completions](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/reference#chat-completions) — Stream-parameter dokumentasjon
|
||||
- [forward-request policy](https://learn.microsoft.com/en-us/azure/api-management/forward-request-policy) — Policy-referanse for forwarding
|
||||
- [Log token usage, prompts, and completions](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-llm-logs) — LLM-logging i APIM
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunder implementerer chat-applikasjoner eller AI-assistenter gjennom APIM og trenger streaming-støtte.
|
||||
- Den absolutt viktigste innstillingen er `buffer-response="false"` i `forward-request`. Uten dette bufres hele SSE-responsen og leveres som én stor blob — som dreper brukeropplevelsen.
|
||||
- Advar om at Consumption-tier IKKE støtter langvarige HTTP-forbindelser som SSE krever. Anbefal v2 eller Premium tier for streaming-scenarier.
|
||||
- For logging av streaming-requests: Bruk `llm-emit-token-metric` for token-metriker (fungerer med streaming). Unngå response body logging som bryter streaming.
|
||||
- Kombiner streaming med retry-policy forsiktig — retry fungerer kun for initial connection failure, ikke for mid-stream feil.
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
# Token-Based Rate Limiting & Quota Policies
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Token-basert rate limiting er den viktigste mekanismen for å kontrollere forbruk av AI-tjenester i Azure API Management. I motsetning til tradisjonell request-basert throttling, teller APIM faktisk antall tokens som konsumeres av hver LLM-forespørsel og håndhever grenser basert på dette. Dette er essensielt for norsk offentlig sektor der flere etater og prosjekter deler Azure OpenAI-ressurser og trenger presis kostnadskontroll.
|
||||
|
||||
APIM tilbyr to parallelle sett med token-policies: ett spesifikt for Azure OpenAI (`azure-openai-token-limit`) og ett generelt for alle LLM-er (`llm-token-limit`). Begge fungerer likt, men det generelle settet støtter også tredjeparts LLM-endepunkter som er kompatible med OpenAI API-formatet. For de fleste scenarier anbefales `llm-token-limit` da det gir størst fleksibilitet.
|
||||
|
||||
I tillegg til tokens per minutt (TPM) rate limits, støtter APIM token-kvoter over lengre perioder (time, dag, uke, måned, år). Kombinasjonen av rate limits og kvoter gir finkornet kontroll: rate limits beskytter mot plutselige spikes, mens kvoter sikrer rettferdig fordeling over tid.
|
||||
|
||||
---
|
||||
|
||||
## Token-telling i APIM
|
||||
|
||||
### Hvordan APIM teller tokens
|
||||
|
||||
APIM bruker to metoder for token-telling:
|
||||
|
||||
| Metode | Tidspunkt | Nøyaktighet | Konfigurasjon |
|
||||
|--------|-----------|-------------|---------------|
|
||||
| **Estimert (prompt)** | Før request sendes til backend | Omtrentlig, basert på tegnantall | `estimate-prompt-tokens="true"` |
|
||||
| **Faktisk (completion)** | Etter respons fra backend | Eksakt, fra `usage`-feltet i respons | Alltid aktiv for completions |
|
||||
|
||||
**Estimering av prompt tokens:**
|
||||
Når `estimate-prompt-tokens="true"` er satt, beregner APIM et estimat av prompt-tokens basert på innholdet i forespørselen. Dette muliggjør pre-validering: hvis estimatet allerede overskrider kvoten, avvises forespørselen umiddelbart uten å bruke backend-ressurser.
|
||||
|
||||
```
|
||||
Token-estimat = f(antall tegn i prompt, modelltype)
|
||||
```
|
||||
|
||||
> **Viktig:** Token-estimatet er en tilnærming. Det faktiske tokenforbruket kan avvike, spesielt for ikke-engelske tekster (norsk bruker typisk 20-40% flere tokens enn engelsk for tilsvarende tekst).
|
||||
|
||||
### Token-flyt i APIM
|
||||
|
||||
```
|
||||
1. Request mottas av APIM gateway
|
||||
2. IF estimate-prompt-tokens=true:
|
||||
Estimer prompt tokens
|
||||
IF estimat > gjenstående kvote:
|
||||
RETURNER 429 umiddelbart (ingen backend-kall)
|
||||
3. Send request til Azure OpenAI backend
|
||||
4. Motta respons med usage-data:
|
||||
{
|
||||
"usage": {
|
||||
"prompt_tokens": 127,
|
||||
"completion_tokens": 350,
|
||||
"total_tokens": 477
|
||||
}
|
||||
}
|
||||
5. Oppdater token-teller med faktiske verdier
|
||||
6. Emit metrikk til Application Insights
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Policy-referanse
|
||||
|
||||
### llm-token-limit (anbefalt for nye implementasjoner)
|
||||
|
||||
```xml
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
token-quota="1000000"
|
||||
token-quota-period="monthly"
|
||||
estimate-prompt-tokens="true"
|
||||
remaining-tokens-variable-name="remainingTokens"
|
||||
remaining-token-quota-variable-name="remainingQuota"
|
||||
tokens-consumed-variable-name="tokensConsumed">
|
||||
</llm-token-limit>
|
||||
```
|
||||
|
||||
**Attributter:**
|
||||
|
||||
| Attributt | Påkrevd | Beskrivelse | Eksempel |
|
||||
|-----------|---------|-------------|---------|
|
||||
| `counter-key` | Ja | Nøkkel for å identifisere konsumenten | `@(context.Subscription.Id)` |
|
||||
| `tokens-per-minute` | Ja | Maks tokens per minutt (TPM) | `50000` |
|
||||
| `token-quota` | Nei | Total tokenkvote for perioden | `1000000` |
|
||||
| `token-quota-period` | Nei | Kvoteperiode | `hourly`, `daily`, `weekly`, `monthly`, `yearly` |
|
||||
| `estimate-prompt-tokens` | Nei | Pre-estimer prompt tokens | `true` / `false` |
|
||||
| `remaining-tokens-variable-name` | Nei | Variabel for gjenstående TPM | `remainingTokens` |
|
||||
| `remaining-token-quota-variable-name` | Nei | Variabel for gjenstående kvote | `remainingQuota` |
|
||||
| `tokens-consumed-variable-name` | Nei | Variabel for brukte tokens | `tokensConsumed` |
|
||||
| `retry-after-variable-name` | Nei | Variabel for retry-after sekunder | `retryAfter` |
|
||||
|
||||
### azure-openai-token-limit (Azure OpenAI-spesifikk)
|
||||
|
||||
```xml
|
||||
<azure-openai-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
estimate-prompt-tokens="true"
|
||||
remaining-tokens-variable-name="remainingTokens">
|
||||
</azure-openai-token-limit>
|
||||
```
|
||||
|
||||
> **Merk:** `azure-openai-token-limit` støtter kun TPM rate limiting, ikke token-kvoter over lengre perioder. Bruk `llm-token-limit` for full kvotestøtte.
|
||||
|
||||
---
|
||||
|
||||
## Counter-key-strategier
|
||||
|
||||
Valg av `counter-key` bestemmer granulariteten av rate limiting:
|
||||
|
||||
### Strategi 1: Per subscription (standard)
|
||||
|
||||
```xml
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000" />
|
||||
```
|
||||
|
||||
**Bruk:** Standard for de fleste scenarier. Hvert team/prosjekt får egen APIM subscription med dedikert kvote.
|
||||
|
||||
### Strategi 2: Per IP-adresse
|
||||
|
||||
```xml
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Request.IpAddress)"
|
||||
tokens-per-minute="10000" />
|
||||
```
|
||||
|
||||
**Bruk:** Beskyttelse mot individuelle klienter som overforbruker. Nyttig for interne applikasjoner.
|
||||
|
||||
### Strategi 3: Per avdeling/etat (custom header)
|
||||
|
||||
```xml
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Request.Headers.GetValueOrDefault("x-etat-id", "default"))"
|
||||
tokens-per-minute="100000"
|
||||
token-quota="5000000"
|
||||
token-quota-period="monthly" />
|
||||
```
|
||||
|
||||
**Bruk:** Offentlig sektor der flere etater deler infrastruktur. Krever at klienter sender header.
|
||||
|
||||
### Strategi 4: Per bruker (JWT claim)
|
||||
|
||||
```xml
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization", "").AsJwt()?.Claims["oid"]?.FirstOrDefault() ?? "anonymous")"
|
||||
tokens-per-minute="5000" />
|
||||
```
|
||||
|
||||
**Bruk:** Individuell brukerbegrensning. Krever JWT-token med brukeridentitet.
|
||||
|
||||
### Strategi 5: Kombinert (subscription + bruker)
|
||||
|
||||
```xml
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id + "-" + context.Request.Headers.GetValueOrDefault("x-user-id", "shared"))"
|
||||
tokens-per-minute="5000" />
|
||||
```
|
||||
|
||||
**Bruk:** Finkornet kontroll der hvert team har en total kvote, men individuelle brukere innen teamet også begrenses.
|
||||
|
||||
---
|
||||
|
||||
## Rate Limit-algoritmer
|
||||
|
||||
### Classic tiers: Sliding Window
|
||||
|
||||
I Classic tiers (Developer, Basic, Standard, Premium) bruker APIM en sliding window-algoritme:
|
||||
|
||||
```
|
||||
Tidslinje:
|
||||
[────────── 60 sek vindu ──────────]
|
||||
^-- request evalueres her
|
||||
|
||||
Tokens brukt i vinduet: 45 000 av 50 000 TPM
|
||||
Ny request med estimert 6 000 tokens → AVVIST (429)
|
||||
Ny request med estimert 4 000 tokens → GODKJENT
|
||||
```
|
||||
|
||||
**Egenskaper:**
|
||||
- Jevn fordeling over tid
|
||||
- Kan gi uventede 429-svar ved burst-trafikk
|
||||
- Teller akkumuleres over glidende 60-sekunders vindu
|
||||
|
||||
### V2 tiers: Token Bucket
|
||||
|
||||
V2 tiers (Basic v2, Standard v2, Premium v2) bruker en token bucket-algoritme:
|
||||
|
||||
```
|
||||
Bucket-kapasitet: 50 000 tokens
|
||||
Refill rate: 50 000 tokens / 60 sek = 833 tokens/sek
|
||||
|
||||
Tidspunkt 0: Bucket = 50 000 (full)
|
||||
Request A: -10 000 → Bucket = 40 000
|
||||
Request B: -15 000 → Bucket = 25 000
|
||||
... 10 sek ...
|
||||
Refill: +8 330 → Bucket = 33 330
|
||||
Request C: -35 000 → AVVIST (overstiger bucket)
|
||||
```
|
||||
|
||||
**Egenskaper:**
|
||||
- Tillater korte bursts opp til bucket-kapasiteten
|
||||
- Jevnere throttling-opplevelse
|
||||
- Mer effektiv for ujevn trafikk (typisk for AI-workloads)
|
||||
|
||||
---
|
||||
|
||||
## Kvoter over lengre perioder
|
||||
|
||||
### Kvote vs. Rate Limit
|
||||
|
||||
| Egenskap | Rate Limit (TPM) | Kvote |
|
||||
|----------|-------------------|-------|
|
||||
| **Tidshorisont** | Per minutt | Time, dag, uke, måned, år |
|
||||
| **Formål** | Beskytt mot spikes | Rettferdig fordeling over tid |
|
||||
| **Counter scope** | Per gateway-instans (regional) | Globalt (på tvers av regioner) |
|
||||
| **HTTP-kode ved overskridelse** | 429 Too Many Requests | 403 Forbidden |
|
||||
| **Retry-After header** | Ja | Ja |
|
||||
|
||||
### Eksempel: Månedlig kvote med daglig rate limit
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- Rate limit: 50 000 TPM -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
estimate-prompt-tokens="true" />
|
||||
|
||||
<!-- Kvote: 2 000 000 tokens per måned -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
token-quota="2000000"
|
||||
token-quota-period="monthly"
|
||||
tokens-per-minute="999999999"
|
||||
estimate-prompt-tokens="true" />
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Kvoteberegning for offentlig sektor
|
||||
|
||||
Eksempel for en etat med 50 ansatte som bruker AI-tjenester:
|
||||
|
||||
| Brukerkategori | Antall | TPM per bruker | Månedlig kvote per bruker | Total månedlig |
|
||||
|----------------|--------|----------------|---------------------------|----------------|
|
||||
| Power users | 5 | 10 000 | 500 000 | 2 500 000 |
|
||||
| Standard users | 30 | 3 000 | 100 000 | 3 000 000 |
|
||||
| Light users | 15 | 1 000 | 30 000 | 450 000 |
|
||||
| **Totalt** | **50** | - | - | **5 950 000** |
|
||||
|
||||
**Buffer-anbefaling:** Legg til 20-30% buffer for uforutsette topper.
|
||||
|
||||
---
|
||||
|
||||
## Multi-region-hensyn
|
||||
|
||||
### Rate limits er regionale
|
||||
|
||||
```
|
||||
APIM Gateway (Norway East) → Rate limit counter: 50 000 TPM
|
||||
APIM Gateway (Sweden Central) → Rate limit counter: 50 000 TPM
|
||||
(separate tellere!)
|
||||
```
|
||||
|
||||
**Viktig:** I multi-region deployments har hver regional gateway sin egen rate limit-teller. En konsument kan potensielt bruke 50 000 TPM i Norway East OG 50 000 TPM i Sweden Central = 100 000 TPM totalt.
|
||||
|
||||
### Kvoter er globale
|
||||
|
||||
```
|
||||
APIM Instance (global)
|
||||
├── Gateway (Norway East) ─┐
|
||||
└── Gateway (Sweden Central) ─┤── Delt kvote-teller: 2 000 000/mnd
|
||||
└── (synkronisert globalt)
|
||||
```
|
||||
|
||||
**Anbefaling for offentlig sektor:** Bruk kvoter for total kostnadskontroll, og rate limits for burst-beskyttelse. Vurder å justere regionale rate limits basert på forventet trafikkfordeling.
|
||||
|
||||
---
|
||||
|
||||
## Feilhåndtering og respons-headers
|
||||
|
||||
### HTTP-responser ved overskridelse
|
||||
|
||||
| Scenario | HTTP-kode | Response header | Body |
|
||||
|----------|-----------|-----------------|------|
|
||||
| TPM rate limit nådd | 429 | `Retry-After: <sekunder>` | Feilmelding med gjenstående tokens |
|
||||
| Kvote brukt opp | 403 | `Retry-After: <sekunder til reset>` | Feilmelding med kvoteinformasjon |
|
||||
| Prompt estimat overskrider | 429 | `Retry-After: <sekunder>` | Avvist uten backend-kall |
|
||||
|
||||
### Bruk av context-variabler
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
remaining-tokens-variable-name="remainingTokens"
|
||||
tokens-consumed-variable-name="tokensConsumed"
|
||||
retry-after-variable-name="retryAfter" />
|
||||
</inbound>
|
||||
|
||||
<outbound>
|
||||
<!-- Inkluder gjenstående tokens i respons-header -->
|
||||
<set-header name="X-Remaining-Tokens" exists-action="override">
|
||||
<value>@(context.Variables.GetValueOrDefault<int>("remainingTokens").ToString())</value>
|
||||
</set-header>
|
||||
<set-header name="X-Tokens-Consumed" exists-action="override">
|
||||
<value>@(context.Variables.GetValueOrDefault<int>("tokensConsumed").ToString())</value>
|
||||
</set-header>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Burst Allowances og Concurrency Control
|
||||
|
||||
### Kombinere token limit med concurrency limit
|
||||
|
||||
For å beskytte mot mange samtidige store requests:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<!-- Maks 10 samtidige requests per subscription -->
|
||||
<limit-concurrency key="@(context.Subscription.Id)" max-count="10">
|
||||
<!-- Token rate limiting innenfor concurrency-grensen -->
|
||||
<llm-token-limit
|
||||
counter-key="@(context.Subscription.Id)"
|
||||
tokens-per-minute="50000"
|
||||
estimate-prompt-tokens="true" />
|
||||
</limit-concurrency>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Burst-håndtering med Token Bucket (V2 tiers)
|
||||
|
||||
Token bucket-algoritmen i V2 tiers tillater naturlig burst-kapasitet:
|
||||
|
||||
| Konfigurasjon | Effektiv burst | Sustained rate |
|
||||
|---------------|----------------|----------------|
|
||||
| TPM=50 000 | Opp til 50 000 tokens i enkelt-request | ~833 tokens/sek |
|
||||
| TPM=100 000 | Opp til 100 000 tokens i enkelt-request | ~1 667 tokens/sek |
|
||||
| TPM=200 000 | Opp til 200 000 tokens i enkelt-request | ~3 333 tokens/sek |
|
||||
|
||||
---
|
||||
|
||||
## Monitorering av token-forbruk
|
||||
|
||||
### Token-metrikk policy
|
||||
|
||||
```xml
|
||||
<outbound>
|
||||
<llm-emit-token-metric namespace="ai-token-usage">
|
||||
<dimension name="Subscription" value="@(context.Subscription.Id)" />
|
||||
<dimension name="Etat" value="@(context.Request.Headers.GetValueOrDefault("x-etat", "ukjent"))" />
|
||||
<dimension name="Model" value="@(context.Request.Headers.GetValueOrDefault("x-model", "default"))" />
|
||||
</llm-emit-token-metric>
|
||||
</outbound>
|
||||
```
|
||||
|
||||
### KQL-spørring for token-forbruk
|
||||
|
||||
```kusto
|
||||
customMetrics
|
||||
| where name == "Total Tokens"
|
||||
| extend etat = tostring(customDimensions["Etat"])
|
||||
| summarize TotalTokens = sum(value) by etat, bin(timestamp, 1h)
|
||||
| order by timestamp desc
|
||||
| render timechart
|
||||
```
|
||||
|
||||
### Azure Monitor Alert for kvote-overskridelse
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Microsoft.Insights/metricAlerts",
|
||||
"properties": {
|
||||
"criteria": {
|
||||
"metricName": "Total Tokens",
|
||||
"metricNamespace": "ai-token-usage",
|
||||
"operator": "GreaterThan",
|
||||
"threshold": 4000000,
|
||||
"timeAggregation": "Total",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "Subscription",
|
||||
"operator": "Include",
|
||||
"values": ["*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"windowSize": "P1D",
|
||||
"evaluationFrequency": "PT1H"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tier-kompatibilitet
|
||||
|
||||
| Policy | Classic | V2 | Consumption | Self-hosted | Workspace |
|
||||
|--------|---------|----|-----------|----------- |-----------|
|
||||
| `azure-openai-token-limit` | Ja | Ja | Nei | Ja | Ja |
|
||||
| `llm-token-limit` | Ja | Ja | Nei | Ja | Ja |
|
||||
| `azure-openai-emit-token-metric` | Ja | Ja | Nei | Ja | Ja |
|
||||
| `llm-emit-token-metric` | Ja | Ja | Nei | Ja | Ja |
|
||||
| `rate-limit-by-key` | Ja | Ja | Nei | Ja | Ja |
|
||||
| `quota-by-key` | Ja | Nei | Nei | Ja | Ja |
|
||||
|
||||
> **Merk:** Token-policies er IKKE tilgjengelige i Consumption tier. For AI-workloads, bruk minimum Basic v2 eller Standard v2.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Anbefalt oppsett for norsk offentlig sektor
|
||||
|
||||
1. **Bruk `llm-token-limit`** fremfor `azure-openai-token-limit` for fremtidig fleksibilitet
|
||||
2. **Aktiver `estimate-prompt-tokens`** for å avvise for store requests tidlig
|
||||
3. **Kombiner TPM rate limit med månedlig kvote** for dobbel beskyttelse
|
||||
4. **Bruk subscription-basert counter-key** som primær granularitet
|
||||
5. **Legg til custom header-dimensjoner** for kostnadsrapportering per etat/prosjekt
|
||||
6. **Sett opp Azure Monitor Alerts** ved 80% kvotebruk
|
||||
7. **Dokumenter kvoteallokeringer** i tjenestekataloger og SLA-er
|
||||
8. **Test med GenAI Gateway Toolkit** for å verifisere policy-oppførsel under last
|
||||
|
||||
---
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- Token rate limiting er den viktigste AI gateway-policyen -- alltid start her når du setter opp APIM for Azure OpenAI. Bruk `llm-token-limit` som standard, med `estimate-prompt-tokens="true"` for tidlig avvisning.
|
||||
- Counter-key-strategien bestemmer granulariteten: subscription-basert for team-nivå, custom headers for etat/prosjekt, JWT claims for bruker-nivå. For offentlig sektor anbefales subscription per team + custom header for kostnadsrapportering.
|
||||
- V2 tiers bruker token bucket-algoritme som håndterer bursts bedre enn sliding window i Classic tiers -- anbefal Standard v2 for nye deployments.
|
||||
- Rate limits er regionale (per gateway-instans), men kvoter er globale. I multi-region oppsett må du dimensjonere rate limits per region, men bruke kvoter for total kostnadskontroll.
|
||||
- Kombiner alltid `llm-token-limit` med `llm-emit-token-metric` for full observabilitet, og sett opp alerts ved 80% kvotebruk for proaktiv kapasitetsstyring.
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
# API Versioning Strategies for AI Endpoints
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** API Management & AI Gateway
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
API-versjonering er kritisk for AI-tjenester der underliggende modeller endres hyppig, nye kapabiliteter legges til og eldre versjoner fases ut. Azure API Management tilbyr tre versjoneringsstrategier (URL-path, header og query string) samt revisjonsstyring for ikke-brytende endringer. For AI-API-er er dette spesielt utfordrende fordi modellversjoner, API-schemaer og responsformater kan endres uavhengig av hverandre.
|
||||
|
||||
For norsk offentlig sektor er kontrollert versjonering essensielt. Offentlige virksomheter har ofte integrerte systemer som er avhengige av stabile API-grensesnitt, og et modellbytte kan gi annerledes output for samme prompt. En robust versjoneringssstrategi sikrer at eksisterende integrasjoner fortsetter a fungere nar nye modeller eller kapabiliteter innfores, og gir forbrukere tid til a migrere kontrollert.
|
||||
|
||||
APIM skiller mellom versjoner (for brytende endringer) og revisjoner (for ikke-brytende endringer). Denne referansen dekker begge konseptene i konteksten av AI-API-er, med praktiske monstre for modellversjonsmapping, migrasjon og avvikling.
|
||||
|
||||
---
|
||||
|
||||
## Versjoneringsstrategier i APIM
|
||||
|
||||
### Tre tilgjengelige skjemaer
|
||||
|
||||
| Skjema | Format | Eksempel |
|
||||
|--------|--------|---------|
|
||||
| **URL Path** | `/{api-path}/v1/...` | `https://api.virksomhet.no/ai/v1/chat/completions` |
|
||||
| **Header** | Custom header | `Api-Version: 2024-08-01` |
|
||||
| **Query String** | URL-parameter | `https://api.virksomhet.no/ai/chat/completions?api-version=2024-08-01` |
|
||||
|
||||
### Anbefaling for AI-API-er
|
||||
|
||||
| Strategi | Fordeler | Ulemper | Anbefalt for |
|
||||
|----------|---------|---------|-------------|
|
||||
| URL Path | Tydelig, selvdokumenterende, lett a route | Mer tungvint a migrere | Offentlige API-er med stabile versjoner |
|
||||
| Header | Ren URL, fleksibelt | Ikke synlig i URL | Interne API-er, programmatisk tilgang |
|
||||
| Query String | Kompatibelt med Azure OpenAI-konvensjon | Kan bli rotete med mange params | Direkte kompatibilitet med Azure OpenAI |
|
||||
|
||||
**Anbefaling:** For AI gateway som wrapper rundt Azure OpenAI, bruk **query string** med `api-version` for a folge Microsofts eksisterende konvensjon. For egne AI-fasade-API-er, bruk **URL path** for tydelighet.
|
||||
|
||||
### Konfigurere versjonering i APIM
|
||||
|
||||
#### URL Path-versjonering
|
||||
|
||||
```bicep
|
||||
resource apiVersionSet 'Microsoft.ApiManagement/service/apiVersionSets@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-gateway-version-set'
|
||||
properties: {
|
||||
displayName: 'AI Gateway API'
|
||||
versioningScheme: 'Segment' // URL path
|
||||
description: 'AI Gateway API versjonert med URL-path'
|
||||
}
|
||||
}
|
||||
|
||||
resource aiApiV1 'Microsoft.ApiManagement/service/apis@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-gateway-v1'
|
||||
properties: {
|
||||
displayName: 'AI Gateway API v1'
|
||||
apiVersion: 'v1'
|
||||
apiVersionSetId: apiVersionSet.id
|
||||
path: 'ai'
|
||||
protocols: [ 'https' ]
|
||||
subscriptionRequired: true
|
||||
}
|
||||
}
|
||||
|
||||
resource aiApiV2 'Microsoft.ApiManagement/service/apis@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-gateway-v2'
|
||||
properties: {
|
||||
displayName: 'AI Gateway API v2'
|
||||
apiVersion: 'v2'
|
||||
apiVersionSetId: apiVersionSet.id
|
||||
path: 'ai'
|
||||
protocols: [ 'https' ]
|
||||
subscriptionRequired: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Header-basert versjonering
|
||||
|
||||
```bicep
|
||||
resource apiVersionSetHeader 'Microsoft.ApiManagement/service/apiVersionSets@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-gateway-header-version-set'
|
||||
properties: {
|
||||
displayName: 'AI Gateway API (Header Versioned)'
|
||||
versioningScheme: 'Header'
|
||||
versionHeaderName: 'Api-Version'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Query String-versjonering
|
||||
|
||||
```bicep
|
||||
resource apiVersionSetQuery 'Microsoft.ApiManagement/service/apiVersionSets@2023-09-01-preview' = {
|
||||
parent: apiManagement
|
||||
name: 'ai-gateway-query-version-set'
|
||||
properties: {
|
||||
displayName: 'AI Gateway API (Query Versioned)'
|
||||
versioningScheme: 'Query'
|
||||
versionQueryName: 'api-version'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Avviklingsfrister (Deprecation Timelines)
|
||||
|
||||
### Livssyklusmodell for AI-API-versjoner
|
||||
|
||||
```
|
||||
[Preview] --> [GA] --> [Deprecated] --> [Retired]
|
||||
| | | |
|
||||
| 6-12 mnd | 6-12 mnd| 3-6 mnd |
|
||||
| | | |
|
||||
Flagg: Flagg: Flagg: Fjernet
|
||||
beta stable deprecated fra gateway
|
||||
```
|
||||
|
||||
### Fasestyring med policies
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<outbound>
|
||||
<base />
|
||||
<!-- Add deprecation headers based on API version -->
|
||||
<choose>
|
||||
<!-- Deprecated version -->
|
||||
<when condition="@(context.Api.Version == "v1")">
|
||||
<set-header name="Deprecation" exists-action="override">
|
||||
<value>true</value>
|
||||
</set-header>
|
||||
<set-header name="Sunset" exists-action="override">
|
||||
<value>Sat, 30 Jun 2026 00:00:00 GMT</value>
|
||||
</set-header>
|
||||
<set-header name="Link" exists-action="override">
|
||||
<value><https://api.virksomhet.no/ai/v2/docs>; rel="successor-version"</value>
|
||||
</set-header>
|
||||
<!-- Log deprecation usage for tracking -->
|
||||
<trace source="api-versioning" severity="warning">
|
||||
<message>@($"Deprecated API v1 called by {context.Subscription.Name}")</message>
|
||||
</trace>
|
||||
</when>
|
||||
<!-- Preview version -->
|
||||
<when condition="@(context.Api.Version == "v3-preview")">
|
||||
<set-header name="x-api-status" exists-action="override">
|
||||
<value>preview</value>
|
||||
</set-header>
|
||||
<set-header name="x-api-warning" exists-action="override">
|
||||
<value>This API version is in preview and may change without notice.</value>
|
||||
</set-header>
|
||||
</when>
|
||||
</choose>
|
||||
</outbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Standard HTTP-headere for versjonsstyring
|
||||
|
||||
| Header | Verdi | RFC |
|
||||
|--------|-------|-----|
|
||||
| `Deprecation` | `true` | RFC 8594 |
|
||||
| `Sunset` | ISO 8601 dato | RFC 8594 |
|
||||
| `Link` | URL til ny versjon | RFC 8288 |
|
||||
| `x-api-status` | `preview` / `ga` / `deprecated` | Custom |
|
||||
|
||||
---
|
||||
|
||||
## Modellversjonsmapping
|
||||
|
||||
### Utfordringen med AI-modellversjoner
|
||||
|
||||
AI-modeller oppdateres uavhengig av API-versjoner:
|
||||
|
||||
| API-versjon | Modellnavn | Faktisk modellversjon | Endring |
|
||||
|-------------|-----------|----------------------|---------|
|
||||
| v1 | gpt-4o | 2024-05-13 | Opprinnelig |
|
||||
| v1 | gpt-4o | 2024-08-06 | Modelloppgradering (transparent) |
|
||||
| v2 | gpt-4o | 2024-11-20 | Ny API + ny modell |
|
||||
| v2 | gpt-4o-mini | 2024-07-18 | Ny modelltype i v2 |
|
||||
|
||||
### Policy: Modellversjonsmapping
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Map API version to specific model deployment -->
|
||||
<set-variable name="apiVersion" value="@(context.Api.Version)" />
|
||||
<set-variable name="requestedModel"
|
||||
value="@(context.Request.Body.As<JObject>(preserveContent: true)?["model"]?.ToString())" />
|
||||
|
||||
<choose>
|
||||
<!-- v1: Map to stable, older model deployments -->
|
||||
<when condition="@((string)context.Variables["apiVersion"] == "v1")">
|
||||
<set-variable name="deployment" value="@{
|
||||
var model = (string)context.Variables["requestedModel"];
|
||||
return model switch {
|
||||
"gpt-4o" => "gpt-4o-2024-05-13-stable",
|
||||
"gpt-4" => "gpt-4-0613-stable",
|
||||
_ => "gpt-4o-2024-05-13-stable"
|
||||
};
|
||||
}" />
|
||||
</when>
|
||||
|
||||
<!-- v2: Map to latest model deployments -->
|
||||
<when condition="@((string)context.Variables["apiVersion"] == "v2")">
|
||||
<set-variable name="deployment" value="@{
|
||||
var model = (string)context.Variables["requestedModel"];
|
||||
return model switch {
|
||||
"gpt-4o" => "gpt-4o-2024-11-20-latest",
|
||||
"gpt-4o-mini" => "gpt-4o-mini-2024-07-18",
|
||||
_ => "gpt-4o-2024-11-20-latest"
|
||||
};
|
||||
}" />
|
||||
</when>
|
||||
</choose>
|
||||
|
||||
<!-- Route to correct deployment -->
|
||||
<rewrite-uri template="@($"/openai/deployments/{context.Variables["deployment"]}/chat/completions")" />
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migreringsstrategier
|
||||
|
||||
### Parallellkjoring av versjoner
|
||||
|
||||
Kjor gammel og ny versjon side om side med gradvis migrering:
|
||||
|
||||
```xml
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Canary: Route percentage of v1 traffic to v2 backend -->
|
||||
<choose>
|
||||
<when condition="@(context.Api.Version == "v1" && new Random().Next(100) < 10)">
|
||||
<!-- 10% of v1 traffic gets v2 backend for shadow testing -->
|
||||
<set-variable name="shadowTest" value="true" />
|
||||
<set-backend-service backend-id="ai-backend-v2" />
|
||||
<set-header name="x-shadow-test" exists-action="override">
|
||||
<value>true</value>
|
||||
</set-header>
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
### Migreringssjekkliste
|
||||
|
||||
| Fase | Handling | Varighet |
|
||||
|------|---------|----------|
|
||||
| 1. Announce | Publiser ny versjon, dokumenter endringer | Uke 0 |
|
||||
| 2. Parallel | Kjor begge versjoner, monitor bruk | Uke 1-12 |
|
||||
| 3. Deprecate | Merk gammel versjon som deprecated | Uke 8 |
|
||||
| 4. Notify | Send varsler til aktive brukere | Uke 8, 16, 22 |
|
||||
| 5. Restrict | Reduser rate limits pa gammel versjon | Uke 20 |
|
||||
| 6. Sunset | Fjern gammel versjon | Uke 24 |
|
||||
|
||||
### KQL: Overvak versjonsbruk
|
||||
|
||||
```kusto
|
||||
ApiManagementGatewayLogs
|
||||
| where TimeGenerated > ago(30d)
|
||||
| extend ApiVersion = tostring(split(ApiId, "-")[-1])
|
||||
| summarize
|
||||
RequestCount = count(),
|
||||
UniqueSubscriptions = dcount(SubscriptionId)
|
||||
by ApiVersion, bin(TimeGenerated, 1d)
|
||||
| order by TimeGenerated desc, ApiVersion asc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Revisjonsstyring for ikke-brytende endringer
|
||||
|
||||
### Revisjoner vs. Versjoner
|
||||
|
||||
| Egenskap | Revisjon | Versjon |
|
||||
|----------|----------|---------|
|
||||
| Type endring | Ikke-brytende | Brytende |
|
||||
| URL-endring | Nei (`;rev=N` valgfri) | Ja (ny versjon i path/header/query) |
|
||||
| Eksempel | Legge til valgfritt felt | Endre responsstruktur |
|
||||
| Klientpavirkning | Ingen (bakoverkompatibelt) | Krever klientoppdatering |
|
||||
| Publisering | Gjor revisjon "current" | Ny API-versjon |
|
||||
| Change log | Valgfri endringslogg | Egen dokumentasjon |
|
||||
|
||||
### Bruke revisjoner for modelloppgraderinger
|
||||
|
||||
Nar en modell oppdateres uten API-endringer (f.eks. GPT-4o far ny snapshot):
|
||||
|
||||
1. Opprett ny revisjon av API-et
|
||||
2. Endre backend-deployment i den nye revisjonen
|
||||
3. Test grundig med nye revisjon
|
||||
4. Gjor revisjon "current" nar validert
|
||||
5. Publiser endringslogg
|
||||
|
||||
```xml
|
||||
<!-- Revision-specific backend for testing -->
|
||||
<policies>
|
||||
<inbound>
|
||||
<base />
|
||||
<!-- Non-current revisions can use different backends for testing -->
|
||||
<choose>
|
||||
<when condition="@(context.Api.Revision == "3")">
|
||||
<set-backend-service backend-id="ai-backend-new-model" />
|
||||
</when>
|
||||
</choose>
|
||||
</inbound>
|
||||
</policies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handtering av brytende endringer
|
||||
|
||||
### Hva er en brytende endring for AI-API-er?
|
||||
|
||||
| Endring | Brytende? | Strategi |
|
||||
|---------|-----------|---------|
|
||||
| Legge til nytt valgfritt felt i response | Nei | Revisjon |
|
||||
| Endre modellnavn | Ja | Ny versjon |
|
||||
| Fjerne felt fra response | Ja | Ny versjon |
|
||||
| Endre feilformat | Ja | Ny versjon |
|
||||
| Endre token-tellemekanisme | Ja | Ny versjon |
|
||||
| Legge til ny operasjon | Nei | Revisjon |
|
||||
| Endre autentiseringsmetode | Ja | Ny versjon |
|
||||
| Oppdatere underliggende modell (samme API) | Avhenger* | Revisjon eller versjon |
|
||||
|
||||
\* Modelloppgraderinger som gir vesentlig annerledes output bor behandles som brytende.
|
||||
|
||||
### Versjonering av OpenAPI-spesifikasjon
|
||||
|
||||
```yaml
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: AI Gateway API
|
||||
version: '2.0'
|
||||
description: |
|
||||
## Endringslogg
|
||||
### v2.0 (2026-02)
|
||||
- Ny modell: gpt-4o-mini
|
||||
- Endret responsformat for token_usage
|
||||
- Fjernet deprecated 'prompt' field (bruk 'messages')
|
||||
|
||||
### v1.0 (2025-06) - DEPRECATED
|
||||
- Opprinnelig versjon
|
||||
- Sunset: 2026-06-30
|
||||
contact:
|
||||
name: AI Platform Team
|
||||
email: ai-platform@virksomhet.no
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Versions in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/api-management-versions) -- versjoneringsguide
|
||||
- [Tutorial: Publish multiple versions of your API](https://learn.microsoft.com/en-us/azure/api-management/api-management-get-started-publish-versions) -- hands-on tutorial
|
||||
- [Revisions in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/api-management-revisions) -- revisjonsstyring
|
||||
- [Tutorial: Use revisions to make nonbreaking changes](https://learn.microsoft.com/en-us/azure/api-management/api-management-get-started-revise-api) -- revisjon-tutorial
|
||||
- [API design - Versioning (Azure Architecture)](https://learn.microsoft.com/en-us/azure/architecture/microservices/design/api-design#api-versioning) -- designprinsipper
|
||||
- [OWASP: Improper inventory management](https://learn.microsoft.com/en-us/azure/api-management/mitigate-owasp-api-threats#improper-inventory-management) -- sikkerhetsanbefalinger
|
||||
- [AI gateway in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities) -- AI gateway-oversikt
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** nar kunden planlegger versjonering av sine AI-API-er, trenger a migrere mellom modellversjoner, eller vil etablere en livssyklusmodell for sine API-endepunkter.
|
||||
- For AI gateway som wrapper rundt Azure OpenAI, anbefal query string-versjonering med `api-version` for kompatibilitet med Microsofts eksisterende konvensjon.
|
||||
- Skill alltid mellom modellversjoner og API-versjoner -- en modelloppgradering er ikke nodvendigvis en API-versjon. Bruk revisjoner for transparente modelloppgraderinger og versjoner for brytende API-endringer.
|
||||
- Anbefal minimum 6 maneders deprecation-periode for norsk offentlig sektor, der integrerte systemer ofte har lange endringssykluser.
|
||||
- Bruk alltid `Deprecation` og `Sunset` HTTP-headere (RFC 8594) for a gi maskinlesbare signaler til klienter om kommende avvikling -- dette lar automatiserte systemer varsle forvaltere.
|
||||
Loading…
Add table
Add a link
Reference in a new issue