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

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

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

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.