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:
Kjell Tore Guttormsen 2026-04-07 17:17:17 +02:00
commit 6a7632146e
490 changed files with 213249 additions and 2 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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).

View file

@ -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.

View file

@ -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.

View file

@ -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.