Home / Articles / APIM in front of agentic Foundry calls with least-privilege RBAC

This article is published in English.

APIM in front of agentic Foundry calls with least-privilege RBAC

Front model invocations with API Management, managed identity, custom data-plane roles, and Function App backends.

2858 words

What we’re building

AI Foundry can host many model families. Building agents on top gets interesting—and messy—when each model has different auth surfaces and when PII/PCI rules apply. Azure API Management (APIM) plus least-privilege identities gives a single front door.

Part 1 — The foundation

Provision the Foundry/OpenAI resources, a Function App that will call models, and APIM that will front the function. Decide regions and private networking up front.

Part 2 — Deploying the models

Deploy chat models you actually need. Record deployment names separately from base model names.

PART 2: Least-privilege custom RBAC

OpenAI — chat completions only

Custom roles that allow only the data actions your function needs beat broad Cognitive Services Contributor assignments.

Anthropic — data actions built-in roles may miss

Some third-party model routes need extra data actions; verify with a failed call’s error body, then patch the role—not the other way around.

RG="rg-foundry-func-demo"
LOCATION="eastus2"
UMI_NAME="umi-foundry-func"
FOUNDRY_NAME="foundry-func-demo"
STORAGE_NAME="stfoundryfuncdemo"
FUNC_NAME="func-foundry-py-$RANDOM"
SUBSCRIPTION_ID=$(az account show --query id -o tsv)

az group create --name $RG --location $LOCATION
az identity create --name $UMI_NAME --resource-group $RG

UMI_CLIENT_ID=$(az identity show --name $UMI_NAME --resource-group $RG --query clientId -o tsv)
UMI_PRINCIPAL_ID=$(az identity show --name $UMI_NAME --resource-group $RG --query principalId -o tsv)
UMI_RESOURCE_ID=$(az identity show --name $UMI_NAME --resource-group $RG --query id -o tsv)

az cognitiveservices account create \
  --name $FOUNDRY_NAME \
  --resource-group $RG \
  --location $LOCATION \
  --kind AIServices \
  --sku S0 \
  --custom-domain $FOUNDRY_NAME \
  --yes

FOUNDRY_ENDPOINT=$(az cognitiveservices account show \
  --name $FOUNDRY_NAME --resource-group $RG \
  --query properties.endpoint -o tsv)

FOUNDRY_ID=$(az cognitiveservices account show \
  --name $FOUNDRY_NAME --resource-group $RG --query id -o tsv)
az cognitiveservices account deployment create \
  --name $FOUNDRY_NAME \
  --resource-group $RG \
  --deployment-name gpt-5-mini \
  --model-name gpt-5-mini \
  --model-version "2025-08-07" \
  --model-format OpenAI \
  --sku-name GlobalStandard \
  --sku-capacity 10

{
  "etag": "\"c39e7d0e-7c32-48a2-ba62-8a897265efbb\"",
  "id": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourceGroups/rg-foundry-func-demo/providers/Microsoft.CognitiveServices/accounts/foundry-func-demo-01/deployments/gpt-5-mini",
  "name": "gpt-5-mini",
  "properties": {
    "callRateLimit": null,
    "capabilities": {
      "agentsV2": "true",
      "area": "US",
      "assistants": "true",
      "chatCompletion": "true",
      "responses": "true"
    },
    "capacitySettings": null,
    "currentCapacity": 10,
    "deploymentState": "Running",
    "dynamicThrottlingEnabled": null,
    "model": {
      "callRateLimit": null,
      "format": "OpenAI",
      "name": "gpt-5-mini",
      "publisher": null,
      "source": null,
      "sourceAccount": null,
      "version": "2025-08-07"
    },
    "parentDeploymentName": null,
    "provisioningState": "Succeeded",
    "raiPolicyName": "Microsoft.DefaultV2",
    "rateLimits": [
      {
        "count": 10.0,
        "dynamicThrottlingEnabled": null,
        "key": "request",
        "matchPatterns": null,
        "minCount": null,
        "renewalPeriod": 60.0
      },
      {
        "count": 10000.0,
        "dynamicThrottlingEnabled": null,
        "key": "token",
        "matchPatterns": null,
        "minCount": null,
        "renewalPeriod": 60.0
      }
    ],
    "routing": null,
    "scaleSettings": null,
    "serviceTier": null,
    "spilloverDeploymentName": null,
    "versionUpgradeOption": "OnceNewDefaultVersionAvailable"
  },
  "resourceGroup": "rg-foundry-func-demo",
  "sku": {
    "capacity": 10,
    "family": null,
    "name": "GlobalStandard",
    "size": null,
    "tier": null
  },
  "systemData": {
    "createdAt": "2026-07-05T18:03:12.346795+00:00",
    "createdBy": "krishnan.sriram@hotmail.com",
    "createdByType": "User",
    "lastModifiedAt": "2026-07-05T18:03:12.346795+00:00",
    "lastModifiedBy": "krishnan.sriram@hotmail.com",
    "lastModifiedByType": "User"
  },
  "tags": null,
  "type": "Microsoft.CognitiveServices/accounts/deployments"
}
FOUNDRY_ID=$(az cognitiveservices account show \
  --name $FOUNDRY_NAME --resource-group $RG --query id -o tsv)

az resource create \
  --id "$FOUNDRY_ID/deployments/claude-opus-4-6" \
  --api-version 2025-10-01-preview \
  --is-full-object \
  --properties '{
    "location": "eastus2",
    "properties": {
      "model": {
        "format": "Anthropic",
        "name": "claude-opus-4-6",
        "version": "1"
      },
      "modelProviderData": {
        "organizationName": "SRK Tech",
        "countryCode": "US",
        "industry": "technology"
      }
    },
    "sku": {
      "name": "GlobalStandard",
      "capacity": 10
    }
  }'
# execution Results
{
  "etag": "\"a5b65a53-a65a-494b-9059-b16dc700b743\"",
  "extendedLocation": null,
  "id": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourceGroups/rg-foundry-func-demo/providers/Microsoft.CognitiveServices/accounts/foundry-func-demo-01/deployments/claude-opus-4-6",
  "identity": null,
  "kind": null,
  "location": null,
  "managedBy": null,
  "name": "claude-opus-4-6",
  "plan": null,
  "properties": {
    "capabilities": {
      "agentsV2": "true",
      "chatCompletion": "true"
    },
    "currentCapacity": 10,
    "deploymentState": "Running",
    "model": {
      "format": "Anthropic",
      "name": "claude-opus-4-6",
      "version": "1"
    },
    "modelProviderData": {
      "countryCode": "US",
      "industry": "technology",
      "organizationName": "SRK Tech"
    },
    "provisioningState": "Succeeded",
    "raiPolicyName": "Microsoft.DefaultV2",
    "rateLimits": [
      {
        "count": 10,
        "key": "request",
        "renewalPeriod": 60
      },
      {
        "count": 10000,
        "key": "token",
        "renewalPeriod": 60
      }
    ],
    "versionUpgradeOption": "OnceNewDefaultVersionAvailable"
  },
  "resourceGroup": "rg-foundry-func-demo",
  "sku": {
    "capacity": 10,
    "family": null,
    "model": null,
    "name": "GlobalStandard",
    "size": null,
    "tier": null
  },
  "systemData": {
    "createdAt": "2026-07-05T18:06:43.6897129Z",
    "createdBy": "krishnan.sriram@hotmail.com",
    "createdByType": "User",
    "lastModifiedAt": "2026-07-05T18:06:43.6897129Z",
    "lastModifiedBy": "krishnan.sriram@hotmail.com",
    "lastModifiedByType": "User"
  },
  "tags": null,
  "type": "Microsoft.CognitiveServices/accounts/deployments"
}
az cognitiveservices account deployment list \
  --name $FOUNDRY_NAME --resource-group $RG -o table
Name             ResourceGroup
---------------  --------------------
gpt-5-mini       rg-foundry-func-demo
claude-opus-4-6  rg-foundry-func-demo
cat > /tmp/foundry-openai-role.json << EOF
{
  "Name": "Foundry OpenAI Chat Inference User",
  "IsCustom": true,
  "Description": "Minimal data-plane access to call chat completions on a Microsoft Foundry OpenAI deployment.",
  "Actions": [],
  "DataActions": [
    "Microsoft.CognitiveServices/accounts/OpenAI/deployments/chat/completions/action"
  ],
  "AssignableScopes": ["/subscriptions/$SUBSCRIPTION_ID"]
}
EOF
az role definition create --role-definition /tmp/foundry-openai-role.json

# Execution results
{
  "assignableScopes": [
    "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe"
  ],
  "createdBy": null,
  "createdOn": "2026-07-05T18:22:39.111105+00:00",
  "description": "Minimal data-plane access to call chat completions on a Microsoft Foundry OpenAI deployment.",
  "id": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/providers/Microsoft.Authorization/roleDefinitions/c7035712-c66d-482d-9fd5-0fc116a2d9f4",
  "name": "c7035712-c66d-482d-9fd5-0fc116a2d9f4",
  "permissions": [
    {
      "actions": [],
      "condition": null,
      "conditionVersion": null,
      "dataActions": [
        "Microsoft.CognitiveServices/accounts/OpenAI/deployments/chat/completions/action"
      ],
      "notActions": [],
      "notDataActions": []
    }
  ],
  "roleName": "Foundry OpenAI Chat Inference User",
  "roleType": "CustomRole",
  "type": "Microsoft.Authorization/roleDefinitions",
  "updatedBy": "28567f42-a049-464e-9211-8f1b270cdf6a",
  "updatedOn": "2026-07-05T18:22:39.111105+00:00"
}

az role assignment create \
  --assignee-object-id $UMI_PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Foundry OpenAI Chat Inference User" \
  --scope $FOUNDRY_ID

# Execution results
{
  "condition": null,
  "conditionVersion": null,
  "createdBy": null,
  "createdOn": "2026-07-05T18:23:42.642379+00:00",
  "delegatedManagedIdentityResourceId": null,
  "description": null,
  "id": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourceGroups/rg-foundry-func-demo/providers/Microsoft.CognitiveServices/accounts/foundry-func-demo-01/providers/Microsoft.Authorization/roleAssignments/580a3b61-aaad-456e-9b3e-6ced977dd378",
  "name": "580a3b61-aaad-456e-9b3e-6ced977dd378",
  "principalId": "a114ced5-b53e-4c42-8949-bddd053610cc",
  "principalType": "ServicePrincipal",
  "resourceGroup": "rg-foundry-func-demo",
  "roleDefinitionId": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/providers/Microsoft.Authorization/roleDefinitions/c7035712-c66d-482d-9fd5-0fc116a2d9f4",
  "scope": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourceGroups/rg-foundry-func-demo/providers/Microsoft.CognitiveServices/accounts/foundry-func-demo-01",
  "type": "Microsoft.Authorization/roleAssignments",
  "updatedBy": "28567f42-a049-464e-9211-8f1b270cdf6a",
  "updatedOn": "2026-07-05T18:23:42.913375+00:00"
}
cat > /tmp/foundry-anthropic-role.json << EOF
{
  "Name": "Foundry Anthropic Inference User",
  "IsCustom": true,
  "Description": "Minimal data-plane access to invoke Claude models on a Microsoft Foundry AIServices resource.",
  "Actions": [],
  "DataActions": [
    "Microsoft.CognitiveServices/accounts/AIServices/providers/action",
    "Microsoft.CognitiveServices/accounts/OpenAI/*/read"
  ],
  "AssignableScopes": ["/subscriptions/$SUBSCRIPTION_ID"]
}
EOF

az role definition create --role-definition /tmp/foundry-anthropic-role.json
# Execution results
{
  "assignableScopes": [
    "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe"
  ],
  "createdBy": null,
  "createdOn": "2026-07-05T18:26:14.247654+00:00",
  "description": "Minimal data-plane access to invoke Claude models on a Microsoft Foundry AIServices resource.",
  "id": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/providers/Microsoft.Authorization/roleDefinitions/323e0475-0098-4161-b03d-69bce7452c9c",
  "name": "323e0475-0098-4161-b03d-69bce7452c9c",
  "permissions": [
    {
      "actions": [],
      "condition": null,
      "conditionVersion": null,
      "dataActions": [
        "Microsoft.CognitiveServices/accounts/AIServices/providers/action",
        "Microsoft.CognitiveServices/accounts/OpenAI/*/read"
      ],
      "notActions": [],
      "notDataActions": []
    }
  ],
  "roleName": "Foundry Anthropic Inference User",
  "roleType": "CustomRole",
  "type": "Microsoft.Authorization/roleDefinitions",
  "updatedBy": "28567f42-a049-464e-9211-8f1b270cdf6a",
  "updatedOn": "2026-07-05T18:26:14.247654+00:00"
}

az role assignment create \
  --assignee-object-id $UMI_PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Foundry Anthropic Inference User" \
  --scope $FOUNDRY_ID

# Execution Results
{
  "condition": null,
  "conditionVersion": null,
  "createdBy": null,
  "createdOn": "2026-07-05T18:28:12.789555+00:00",
  "delegatedManagedIdentityResourceId": null,
  "description": null,
  "id": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourceGroups/rg-foundry-func-demo/providers/Microsoft.CognitiveServices/accounts/foundry-func-demo-01/providers/Microsoft.Authorization/roleAssignments/18e509dc-b539-49d8-a2e6-65a12415e206",
  "name": "18e509dc-b539-49d8-a2e6-65a12415e206",
  "principalId": "a114ced5-b53e-4c42-8949-bddd053610cc",
  "principalType": "ServicePrincipal",
  "resourceGroup": "rg-foundry-func-demo",
  "roleDefinitionId": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/providers/Microsoft.Authorization/roleDefinitions/323e0475-0098-4161-b03d-69bce7452c9c",
  "scope": "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourceGroups/rg-foundry-func-demo/providers/Microsoft.CognitiveServices/accounts/foundry-func-demo-01",
  "type": "Microsoft.Authorization/roleAssignments",
  "updatedBy": "28567f42-a049-464e-9211-8f1b270cdf6a",
  "updatedOn": "2026-07-05T18:28:13.050559+00:00"
}
az storage account create \
  --name $STORAGE_NAME \
  --resource-group $RG \
  --location $LOCATION \
  --sku Standard_LRS
az functionapp create \
  --name $FUNC_NAME \
  --resource-group $RG \
  --storage-account $STORAGE_NAME \
  --consumption-plan-location $LOCATION \
  --runtime python \
  --runtime-version 3.12 \
  --functions-version 4 \
  --os-type Linux
az functionapp identity assign \
  --name $FUNC_NAME \
  --resource-group $RG \
  --identities $UMI_RESOURCE_ID
# Execution results
{
  "principalId": null,
  "tenantId": null,
  "type": "UserAssigned",
  "userAssignedIdentities": {
    "/subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourcegroups/rg-foundry-func-demo/providers/Microsoft.ManagedIdentity/userAssignedIdentities/umi-foundry-func": {
      "clientId": "e7c8a6b7-bd4e-43c7-b49b-d9d6a9e01821",
      "principalId": "a114ced5-b53e-4c42-8949-bddd053610cc"
    }
  }
}
az functionapp config appsettings set \
  --name $FUNC_NAME \
  --resource-group $RG \
  --settings \
    FOUNDRY_ENDPOINT="$FOUNDRY_ENDPOINT" \
    FOUNDRY_DEPLOYMENT="gpt-5-mini" \
    FOUNDRY_API_VERSION="2024-12-01-preview" \
    CLAUDE_BASE_URL="https://${FOUNDRY_NAME}.services.ai.azure.com/anthropic" \
    CLAUDE_OPUS_DEPLOYMENT="claude-opus-4-6" \
    LANGUAGE_ENDPOINT="$FOUNDRY_ENDPOINT" \
    LANGUAGE_API_VERSION="2024-11-01" \
    UMI_CLIENT_ID="$UMI_CLIENT_ID"
az functionapp config appsettings list \
  --name $FUNC_NAME --resource-group $RG \
  --query "[].name" -o tsv

# Execution results
FUNCTIONS_WORKER_RUNTIME
FUNCTIONS_EXTENSION_VERSION
AzureWebJobsStorage
WEBSITE_CONTENTAZUREFILECONNECTIONSTRING
WEBSITE_CONTENTSHARE
AzureWebJobsDashboard
FOUNDRY_ENDPOINT
FOUNDRY_DEPLOYMENT
FOUNDRY_API_VERSION
CLAUDE_BASE_URL
CLAUDE_OPUS_DEPLOYMENT
LANGUAGE_ENDPOINT
LANGUAGE_API_VERSION
UMI_CLIENT_ID
mkdir foundry-llm-function-py && cd foundry-llm-function-py

func new --name AskLLM --template "HTTP trigger" --authlevel function
azure-functions
azure-identity
openai
anthropic
requests
import azure.functions as func
import json
import logging
import os

from azure.identity import ManagedIdentityCredential, get_bearer_token_provider
from openai import AzureOpenAI
from anthropic import AnthropicFoundry

app = func.FunctionApp()

# --- Shared config ---
FOUNDRY_ENDPOINT = os.environ.get("FOUNDRY_ENDPOINT")
FOUNDRY_API_VERSION = os.environ.get("FOUNDRY_API_VERSION", "2024-12-01-preview")
CLAUDE_BASE_URL = os.environ.get("CLAUDE_BASE_URL")
UMI_CLIENT_ID = os.environ.get("UMI_CLIENT_ID")

# --- Per-model deployment names ---
GPT_DEPLOYMENT = os.environ.get("FOUNDRY_DEPLOYMENT")            # e.g. "gpt-5-mini"
CLAUDE_OPUS_DEPLOYMENT = os.environ.get("CLAUDE_OPUS_DEPLOYMENT")    # e.g. "claude-opus-4-6"

DEFAULT_MODEL = "gpt"

# --- Single UMI credential, shared by both clients ---
_credential = ManagedIdentityCredential(client_id=UMI_CLIENT_ID)

_openai_token_provider = get_bearer_token_provider(
    _credential, "https://cognitiveservices.azure.com/.default"
)
_claude_token_provider = get_bearer_token_provider(
    _credential, "https://cognitiveservices.azure.com/.default"
)

_openai_client = AzureOpenAI(
    azure_endpoint=FOUNDRY_ENDPOINT,
    api_version=FOUNDRY_API_VERSION,
    azure_ad_token_provider=_openai_token_provider,
)

_claude_client = AnthropicFoundry(
    azure_ad_token_provider=_claude_token_provider,
    base_url=CLAUDE_BASE_URL,
)

# --- Model registry: alias -> (provider, deployment name) ---
MODEL_REGISTRY = {
    "gpt": ("openai", GPT_DEPLOYMENT),
    "opus": ("anthropic", CLAUDE_OPUS_DEPLOYMENT),
}


def _call_openai(deployment: str, query: str, user_context: str | None) -> str:
    messages = []
    if user_context:
        messages.append({"role": "system", "content": str(user_context)})
    messages.append({"role": "user", "content": query})

    result = _openai_client.chat.completions.create(
        model=deployment,
        messages=messages,
    )
    return result.choices[0].message.content or ""


def _call_claude(deployment: str, query: str, user_context: str | None) -> str:
    kwargs = {
        "model": deployment,
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": query}],
    }
    if user_context:
        kwargs["system"] = [{"type": "text", "text": str(user_context)}]

    result = _claude_client.messages.create(**kwargs)
    return "".join(block.text for block in result.content if block.type == "text")


@app.route(route="ask", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
def AskLLM(req: func.HttpRequest) -> func.HttpResponse:
    try:
        body = req.get_json()
    except ValueError:
        return func.HttpResponse(
            json.dumps({"error": "Request body must be valid JSON"}),
            status_code=400,
            mimetype="application/json",
        )

    query = body.get("query")
    user_context = body.get("context")
    model_alias = (body.get("model") or DEFAULT_MODEL).strip().lower()

    if not query or not isinstance(query, str):
        return func.HttpResponse(
            json.dumps({"error": '"query" (string) is required in the request body'}),
            status_code=400,
            mimetype="application/json",
        )

    if model_alias not in MODEL_REGISTRY:
        return func.HttpResponse(
            json.dumps({
                "error": f'"model" must be one of: {", ".join(MODEL_REGISTRY.keys())}',
            }),
            status_code=400,
            mimetype="application/json",
        )

    provider, deployment = MODEL_REGISTRY[model_alias]

    if not deployment:
        return func.HttpResponse(
            json.dumps({"error": f'No deployment configured for model "{model_alias}"'}),
            status_code=500,
            mimetype="application/json",
        )

    try:
        if provider == "openai":
            answer = _call_openai(deployment, query, user_context)
        else:
            answer = _call_claude(deployment, query, user_context)

        return func.HttpResponse(
            json.dumps({"answer": answer, "model": model_alias}),
            status_code=200,
            mimetype="application/json",
        )
    except Exception as e:
        logging.exception(f"Foundry call failed for model={model_alias}")
        return func.HttpResponse(
            json.dumps({"error": "Failed to get a response from the LLM", "detail": str(e)}),
            status_code=500,
            mimetype="application/json",
        )
func azure functionapp publish $FUNC_NAME

Getting site publishing info...
[2026-07-05T23:43:36.764Z] Starting the function app deployment...
Removing WEBSITE_CONTENTAZUREFILECONNECTIONSTRING app setting.
Removing WEBSITE_CONTENTSHARE app setting.
Creating archive for current directory...
Performing remote build for functions project.
Creating placeholder blob for linux consumption function app...
.....................
.....................
.....................
SCM_RUN_FROM_PACKAGE placeholder blob scm-latest-func-foundry-py-19514.zip located
Uploading built content /home/site/artifacts/functionappartifact.squashfs for linux consumption function app...
Resetting all workers for func-foundry-py-19514.azurewebsites.net
Deployment successful. deployer = Push-Deployer deploymentPath = Functions App ZipDeploy. Extract zip. Remote build.
Remote build succeeded!
[2026-07-05T23:44:23.731Z] Syncing triggers...
Functions in func-foundry-py-19514:
    AskLLM - [httpTrigger]
        Invoke url: https://func-foundry-py-19514.azurewebsites.net/api/ask
FUNC_KEY=$(az functionapp keys list \
  --name $FUNC_NAME --resource-group $RG \
  --query "functionKeys.default" -o tsv)
curl -X POST "https://$FUNC_NAME.azurewebsites.net/api/ask?code=$FUNC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "What is the capital of France?"}'

# Execution results
{"answer": "The capital of France is Paris.", "model": "gpt"}
curl -X POST "https://$FUNC_NAME.azurewebsites.net/api/ask?code=$FUNC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "What is the capital of France?", "model": "opus"}'

# Execution results
{"answer": "The capital of France is **Paris**.", "model": "opus"}
az group delete --name $RG --yes --no-wait
az role definition delete --name "Foundry OpenAI Chat Inference User"
az role definition delete --name "Foundry Anthropic Inference User"

PART 3 — Azure Function

1–2. Storage + Python Function App

3. Attach user-assigned managed identity

4. App settings

Endpoint URLs, deployment names, APIM keys or audience settings—never commit secrets.

5–7. Scaffold, requirements.txt, function_app.py, deploy

8. Test through APIM

Call the APIM route, not the function URL, so policies (rate limit, JWT, IP filter) actually apply.

Troubleshooting reference

401/403 usually mean RBAC or wrong deployment name. 404 often means region/model mismatch. Timeout storms often mean the function cold-starts while APIM’s backend timeout is too low.

Lessons learnt

Centralize model access behind APIM; use managed identity to the models; keep custom RBAC tight; test every new model family’s data actions; log correlation IDs from APIM to the function to the model call.

Agentic development without an API front door recreates key sprawl. APIM is where agent traffic becomes operable.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Policy tip: put token budgets and per-principal rate limits in APIM so a misbehaving agent loop cannot bypass application-level caps by calling the function directly—block direct function ingress in production networks.

Separate environments need separate APIM products and identities. Sharing one Foundry deployment across dev and prod to “save money” usually leaks prompts and breaks blast-radius isolation.

Document which principals may create model deployments versus which may only invoke them. Invocation rights without deployment rights is the usual healthy split.

When adding a new model family, update the custom role, APIM backend, function settings, and a smoke test in the same PR. Partial rolls are how 403s appear only on Fridays.

Prefer correlation headers from APIM through the function into model SDK logging so a single support ticket can stitch the full path.

Separate environments need separate APIM products and identities. Sharing one Foundry deployment across dev and prod to “save money” usually leaks prompts and breaks blast-radius isolation.

Document which principals may create model deployments versus which may only invoke them. Invocation rights without deployment rights is the usual healthy split.

When adding a new model family, update the custom role, APIM backend, function settings, and a smoke test in the same PR. Partial rolls are how 403s appear only on Fridays.

Prefer correlation headers from APIM through the function into model SDK logging so a single support ticket can stitch the full path.

Separate environments need separate APIM products and identities. Sharing one Foundry deployment across dev and prod to “save money” usually leaks prompts and breaks blast-radius isolation.

Document which principals may create model deployments versus which may only invoke them. Invocation rights without deployment rights is the usual healthy split.

When adding a new model family, update the custom role, APIM backend, function settings, and a smoke test in the same PR. Partial rolls are how 403s appear only on Fridays.

Prefer correlation headers from APIM through the function into model SDK logging so a single support ticket can stitch the full path.

Separate environments need separate APIM products and identities. Sharing one Foundry deployment across dev and prod to “save money” usually leaks prompts and breaks blast-radius isolation.