# 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 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` |

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