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.
23 KiB
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
- Model-agnostiske API-schemaer
- Header Rewriting
- Payload-transformasjon
- Error Response Normalization
- Versjonstranslasjon
- Policy Fragments for Reuse
- Bicep: Oppsett av transformasjons-API
- Referanser
- 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.
Designmonster: Facade API Schema
Definer et internt standardskjema som alle AI-API-er mapper til:
{
"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
<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:
<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:
<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:
<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
<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:
<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:
<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
<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:
<!-- 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:
<policies>
<inbound>
<base />
<include-fragment fragment-id="ai-standard-headers" />
<!-- Additional inbound policies -->
</inbound>
</policies>
Bicep: Oppsett av transformasjons-API
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 -- oversikt over policy-konseptet
- API Management policy reference - Transformation -- komplett liste over transformasjonspolicies
- Set body policy -- detaljert dokumentasjon for set-body
- Set header policy -- header-manipulering
- Rewrite URI policy -- URL-omskriving
- AI gateway in Azure API Management -- AI gateway-oversikt
- Policy fragments in API Management -- gjenbrukbare policy-fragmenter
- Tutorial: Transform and protect your 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-contentpolicy 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.