ms-ai-architect/skills/ms-ai-engineering/references/api-management/request-response-transformation-ai.md
Kjell Tore Guttormsen ddce43d8b2 feat(ms-ai-architect): Spor 1 — Port-1-substrat migrert på 4 ikke-advisor-skills (243 Source + 327 Type + 325 TOC + stale-verified poison fjernet) [skip-docs]
Steg 9 (R4): unified migrate-corpus.mjs --write over engineering/governance/
infrastructure/security. 327 filer mutert, verified=null, prosa byte-identisk
(fra første ## seksjon), advisor urørt (0 endringer).

To applier-fixes oppdaget under kjøring (TDD, RED→GREEN):
- insertHeaderFields: anker faller nå tilbake når en meta-linje selv passerer
  500B (2 filer pakket et avsnitt i **Status:** → Type/Source landet utenfor
  scan-vinduet, applierens post-write-assertion fanget + restaurerte).
- normalizeStaleVerified: fjerner nå ALLE stale non-date **Verified:** i
  500B-vinduet, inkl. stray body-dup rett under --- (9 mlops-genaiops-filer var
  ellers falskt "verified"/fresh, droppet fra worklist). Operatør-godkjent
  utvidelse av carve-out; kun stray metadata-linjer, aldri prosa.

test-transform-criterion: precondition oppdatert til post-migrasjons-sannhet
(fila bærer nå Source). Suite 728/728 grønn.
2026-07-04 10:19:11 +02:00

588 lines
23 KiB
Markdown

# Request/Response Transformation for AI APIs
**Last updated:** 2026-06-24
**Status:** GA
**Category:** API Management & AI Gateway
**Type:** reference
**Source:** https://learn.microsoft.com/azure/api-management/genai-gateway-capabilities
---
## Innhold
- [Introduksjon](#introduksjon)
- [Model-agnostiske API-schemaer](#model-agnostiske-api-schemaer)
- [Header Rewriting](#header-rewriting)
- [Payload-transformasjon](#payload-transformasjon)
- [Error Response Normalization](#error-response-normalization)
- [Versjonstranslasjon](#versjonstranslasjon)
- [Policy Fragments for Reuse](#policy-fragments-for-reuse)
- [Bicep: Oppsett av transformasjons-API](#bicep-oppsett-av-transformasjons-api)
- [Referanser](#referanser)
- [For Cosmo](#for-cosmo)
## 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 Direktoratet for digital tjenesteutvikling, 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` |
> **Innebygd alternativ — unified model API (Preview):** APIM tilbyr nå en _unified model API_ som gir facade-mønsteret nedenfor som innebygd funksjon: den eksponerer flere LLM-backends (OpenAI Chat Completions + Anthropic Messages) bak ett OpenAI-kompatibelt endepunkt, med automatisk format-translasjon, modell-aliaser, failover på tvers av leverandører og felles policy-governance. Vurder denne før du bygger custom transformasjon-policies manuelt. Foreløpig Preview (i classic-tiers via AI Gateway early-release-kanal). Se [Create and manage a unified model API](https://learn.microsoft.com/azure/api-management/unified-model-api).
### 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.