AI coding assistants are easy to pilot and harder to govern.
An individual developer can usually get a tool working with a local API key, a base URL, and a few environment variables. That is fine for experimentation. It is not enough for enterprise deployment.
Once tools like Claude Code and Codex become part of the standard developer workflow, the questions change:
How do we authenticate users?
How do we avoid distributing static model API keys?
How do we route traffic through approved infrastructure?
How do we apply per-user or per-team controls?
How do we report usage by user, department, model, and business unit?
How do we preserve the developer experience while adding governance?
One pattern that works well is to use Azure API Management (APIM) as an enterprise AI Gateway in front of Microsoft Foundry.
In this model, Claude Code and Codex still run locally for developers, but their traffic flows through APIM. Users authenticate with Microsoft Entra. APIM validates the user, applies policy, strips unsafe headers, routes to the correct Foundry backend, calls Foundry with managed identity, and writes telemetry to Log Analytics.
The key principle is:
User identity authenticates to APIM.
APIM identity authenticates to Foundry.
That separation is what makes the pattern useful. Developers do not need backend API keys on their machines. Foundry does not need to trust every endpoint. The enterprise gets a central control plane for authentication, routing, governance, quotas, and observability.
Reference architecture
At a high level, the flow looks like this:
Developer machine
Claude Code / Codex
-> Azure CLI token for https://ai.azure.com
-> Authorization: Bearer <Entra token>
Azure API Management
-> validate Microsoft Entra JWT
-> extract user identity claims
-> set reporting headers
-> remove client API-key headers
-> apply rate/token policy
-> route to Foundry API/backend
-> authenticate to Foundry with APIM managed identity
Microsoft Foundry
-> receives request from APIM
-> returns model response
Azure Monitor / Log Analytics
-> ApiManagementGatewayLogs
-> ApiManagementGatewayLlmLog
-> join by CorrelationId
Here is the same thing as a Mermaid diagram:
flowchart LR
Dev[Developer Machine] --> CC[Claude Code / Codex]
CC -->|az account get-access-token<br/>resource: https://ai.azure.com| Token[Microsoft Entra Token]
CC -->|Authorization: Bearer token| APIM[Azure API Management]
APIM --> JWT[validate-jwt]
JWT --> Claims[Extract oid / upn / aud]
Claims --> Headers[Set reporting headers]
Headers --> Cleanup[Strip client API-key headers]
Cleanup --> Policy[Apply quota / routing policy]
Policy -->|Managed identity| Foundry[Microsoft Foundry]
APIM --> Logs[Log Analytics]
Logs --> Gateway[ApiManagementGatewayLogs]
Logs --> LLM[ApiManagementGatewayLlmLog]
Gateway --> Join[Join by CorrelationId]
LLM --> Join
Why APIM is the right control point
Direct-to-model access works, but it creates governance problems:
- Static API keys are hard to rotate, audit, and attribute.
- Local configuration can drift across machines.
- Direct model traffic bypasses centralized controls.
- Usage reporting by user, department, model, or business unit is difficult.
- Quotas and disablement need a consistent enforcement point.
- Different groups may need separate Foundry resources or routing paths.
APIM gives you a central policy layer where you can:
- Validate Microsoft Entra JWTs.
- Extract stable identity claims.
- Route to different Foundry APIs and backends.
- Strip stale or unsafe client credential headers.
- Apply rate limits and token quotas.
- Use managed identity for backend access.
- Log gateway and LLM token telemetry to Log Analytics.
- Enrich logs with user, client, department, manager, or API-route metadata.
In other words:
APIM is not just a proxy.
APIM is the enterprise control plane for AI traffic.
The APIM request flow
Every request should move through the gateway in a predictable order:
1. Client sends request to APIM
Authorization: Bearer <Entra token>
2. APIM validates the JWT
- signature
- issuer
- audience
- tenant
3. APIM extracts identity claims
- oid
- upn / preferred_username
- aud
4. APIM creates reporting variables and headers
- x-entra-oid
- x-entra-upn
- x-client-type
5. APIM removes client credential headers
- x-api-key
- api-key
- anthropic-api-key
- Ocp-Apim-Subscription-Key
6. APIM applies governance policy
- token quotas
- rate limits
- optional user disablement
- route-specific controls
7. APIM forwards to Foundry using managed identity
8. APIM emits gateway and LLM token logs
9. Log Analytics joins request identity with token usage by CorrelationId
That flow is the heart of the architecture.
Client authentication with Azure CLI
The developer signs in locally with Azure CLI:
az login --tenant <tenant-id>
If the user does not have Azure subscriptions in that tenant, use:
az login --tenant <tenant-id> --allow-no-subscriptions
The local helper then requests a token for Azure AI:
az account get-access-token \
--tenant <tenant-id> \
--resource https://ai.azure.com \
--query accessToken \
-o tsv
The client sends that token to APIM:
Authorization: Bearer <access-token>
This token is for APIM validation. It proves who the user is. It is not the credential APIM uses to call Foundry.
Claude Code managed settings
Claude Code supports managed settings, which makes it suitable for enterprise deployment through tools like Jamf, Intune, Group Policy, or other device management systems.
The managed configuration should:
- Point Claude Code at the APIM Foundry gateway URL.
- Configure an
apiKeyHelper. - Set model defaults.
- Avoid static API keys.
Example managed settings:
{
"apiKeyHelper": "/usr/local/bin/claude-azure-ai-token",
"env": {
"CLAUDE_CODE_USE_FOUNDRY": "1",
"ANTHROPIC_FOUNDRY_BASE_URL": "https://<apim-name>.azure-api.net/<foundry-api-path>/anthropic",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8",
"CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "300000"
},
"companyAnnouncements": [
"Claude Code is configured to use Microsoft Foundry through the enterprise APIM gateway. Run az login before starting Claude Code."
]
}
The helper script only needs to output the access token:
#!/bin/sh
set -eu
az account get-access-token \
--tenant <tenant-id> \
--resource https://ai.azure.com \
--query accessToken \
-o tsv
Common managed settings locations:
macOS:
/Library/Application Support/ClaudeCode/managed-settings.d/10-foundry-apim.json
Windows:
C:\Program Files\ClaudeCode\managed-settings.json
On Windows, keep the helper itself under a path without spaces:
C:\ProgramData\ClaudeCode\claude-azure-ai-token.cmd
Avoid pointing apiKeyHelper at a path under:
C:\Program Files\...
Some clients invoke helpers as command strings. A space in the path can prevent execution, causing APIM to receive a request with no bearer token.
In APIM logs, that usually looks like:
401 TokenNotPresent
JWT not present
Codex provider configuration
Codex can use the same Entra-token pattern through its provider auth command.
Example configuration:
model = "gpt-5.5"
model_provider = "enterprise-codex"
model_reasoning_effort = "medium"
[model_providers.enterprise-codex]
name = "Enterprise Codex via APIM"
base_url = "https://<apim-name>.azure-api.net/<openai-api-path>/openai/v1"
wire_api = "responses"
[model_providers.enterprise-codex.auth]
command = "/usr/local/bin/codex-azure-openai-token"
timeout_ms = 5000
refresh_interval_ms = 300000
The auth command returns the Azure AI token:
az account get-access-token \
--tenant <tenant-id> \
--resource https://ai.azure.com \
--query accessToken \
-o tsv
This gives Claude Code and Codex the same enterprise authentication model:
Local Entra sign-in.
Bearer token to APIM.
APIM managed identity to Foundry.
APIM policy: validate the JWT
At the gateway, validate the incoming bearer token before forwarding anything to Foundry.
<validate-jwt header-name="Authorization"
require-scheme="Bearer"
failed-validation-httpcode="401"
failed-validation-error-message="Unauthorized. Run az login and try again."
output-token-variable-name="user-jwt">
<openid-config url="https://login.microsoftonline.com/<tenant-id>/.well-known/openid-configuration" />
<audiences>
<audience>https://ai.azure.com</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/<tenant-id>/</issuer>
<issuer>https://login.microsoftonline.com/<tenant-id>/v2.0</issuer>
</issuers>
</validate-jwt>
For Claude Code and Codex using Azure CLI tokens, the important audience is:
https://ai.azure.com
If you also route an SSO-based desktop client through the same gateway, you can add that Entra application client ID as another accepted audience and map it to a different client type.
APIM policy: extract identity claims
After validate-jwt succeeds, APIM can read claims from the token.
<set-variable name="userOid"
value="@(((Jwt)context.Variables["user-jwt"]).Claims.GetValueOrDefault("oid", "unknown"))" />
<set-variable name="userUpn"
value="@(((Jwt)context.Variables["user-jwt"]).Claims.GetValueOrDefault("upn", ((Jwt)context.Variables["user-jwt"]).Claims.GetValueOrDefault("preferred_username", "unknown")))" />
<set-variable name="tokenAudience"
value="@(((Jwt)context.Variables["user-jwt"]).Claims.GetValueOrDefault("aud", "unknown"))" />
<set-variable name="clientType"
value="@(((string)context.Variables["tokenAudience"]) == "https://ai.azure.com" ? "Claude Code / Codex" : "Other AI Client")" />
The most important claim is:
oid
That is the Microsoft Entra object ID. It is stable and works well for quota keys, joins, and reporting. UPNs and email addresses are useful for human-readable reports, but they can change.
APIM policy: add loggable identity headers
APIM diagnostics can capture selected request and response headers. A useful pattern is to expose identity and client metadata as response headers, then configure diagnostics to log those headers.
<set-header name="x-entra-oid" exists-action="override">
<value>@((string)context.Variables["userOid"])</value>
</set-header>
<set-header name="x-entra-upn" exists-action="override">
<value>@((string)context.Variables["userUpn"])</value>
</set-header>
<set-header name="x-client-type" exists-action="override">
<value>@((string)context.Variables["clientType"])</value>
</set-header>
These headers are not for Foundry. They are for observability.
Once logged, they let you answer:
- Who used the gateway?
- Which client did they use?
- Which APIM API path did they hit?
- How many tokens did they consume?
- Which departments or business units are driving usage?
APIM policy: capture response usage and cache tokens
ApiManagementGatewayLlmLog records prompt, completion, and total tokens, but the live Anthropic response contains a richer usage object that includes prompt-cache read and write token counts.
Example response usage:
{
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 5464
},
"cache_creation_input_tokens": 5464,
"cache_read_input_tokens": 0,
"input_tokens": 9,
"output_tokens": 1,
"service_tier": "standard"
}
That means APIM can capture request-level cache telemetry without logging prompt or completion bodies. The outbound policy can inspect successful responses with preserveContent: true and write the numeric fields into response headers. For Claude Code, support both non-streaming JSON and streaming text/event-stream responses because the usage object can appear inside SSE data: {...} events:
x-llm-input-tokens
x-llm-output-tokens
x-llm-cache-write-tokens
x-llm-cache-read-tokens
x-llm-cache-write-5m-tokens
x-llm-cache-write-1h-tokens
x-llm-service-tier
x-llm-inference-geo
Those headers can then be captured by APIM diagnostics and joined back to identity and token rows with CorrelationId.
APIM policy: strip client credential headers
This is one of the most important parts of the design.
APIM is the authentication boundary. Users should not send static API keys or APIM subscription keys through to Foundry.
Remove API-key style headers before backend forwarding:
<set-header name="x-api-key" exists-action="delete" />
<set-header name="api-key" exists-action="delete" />
<set-header name="anthropic-api-key" exists-action="delete" />
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
This protects against stale local configuration. A user may have an old direct-Foundry or direct-Anthropic setting on their machine. Without header cleanup, APIM may validate the user's Entra token successfully, but Foundry may still reject the backend call because it received conflicting client auth headers.
The rule should be:
Client credentials stop at APIM.
Backend credentials are APIM managed identity.
Backend authentication with managed identity
Each Foundry resource can be configured as an APIM backend.
Backend ID:
<foundry-resource-name>
Backend URL:
https://<foundry-resource-name>.services.ai.azure.com/
Managed identity resource:
https://ai.azure.com/
The APIM policy selects the backend:
<set-backend-service backend-id="<foundry-backend-id>" />
Grant APIM's managed identity access to the Foundry resource. For example:
Cognitive Services User
This is what lets APIM call Foundry without exposing backend credentials to users or endpoints.
Routing multiple Foundry targets
You can expose separate APIM API paths for different Foundry resources, teams, or business units.
Example Anthropic-compatible routes:
https://<apim-name>.azure-api.net/team-a-foundry/anthropic
https://<apim-name>.azure-api.net/team-b-foundry/anthropic
https://<apim-name>.azure-api.net/team-c-foundry/anthropic
Example OpenAI-compatible routes for Codex:
https://<apim-name>.azure-api.net/team-a-openai/openai/v1
https://<apim-name>.azure-api.net/team-b-openai/openai/v1
https://<apim-name>.azure-api.net/team-c-openai/openai/v1
The route becomes a useful reporting dimension. Even if users never label sessions or use cases manually, APIM can infer the target from ApiId.
Diagnostics and Log Analytics
For this scenario, two APIM log tables matter most:
ApiManagementGatewayLogs
Request and response metadata, status codes, caller IP, headers, CorrelationId
ApiManagementGatewayLlmLog
Model/deployment usage, prompt tokens, completion tokens, total tokens, CorrelationId
The bridge between the two is:
CorrelationId
To make identity fields available in ApiManagementGatewayLogs, configure APIM diagnostics to capture the response headers set by policy.
Example diagnostic fragment:
{
"properties": {
"frontend": {
"response": {
"headers": [
"x-entra-oid",
"x-entra-upn",
"x-client-type",
"x-llm-input-tokens",
"x-llm-output-tokens",
"x-llm-cache-write-tokens",
"x-llm-cache-read-tokens",
"x-llm-cache-write-5m-tokens",
"x-llm-cache-write-1h-tokens",
"x-llm-service-tier",
"x-llm-inference-geo"
]
}
}
}
}
If you choose to log custom request headers, configure frontend.request.headers instead.
For AI traffic, be careful with body logging. If the goal is token accounting and attribution, you usually do not need to log prompt and completion content. Identity metadata plus token telemetry is often enough for operational reporting.
Query identity and token usage
Once diagnostics are configured, join gateway logs to LLM token logs by CorrelationId.
let gateway =
ApiManagementGatewayLogs
| where TimeGenerated > ago(24h)
| where _ResourceId has "<apim-name>"
| extend
EntraOID = tostring(ResponseHeaders["x-entra-oid"]),
EntraUPN = tostring(ResponseHeaders["x-entra-upn"]),
ClientType = tostring(ResponseHeaders["x-client-type"]),
LlmInputTokens = toint(ResponseHeaders["x-llm-input-tokens"]),
LlmOutputTokens = toint(ResponseHeaders["x-llm-output-tokens"]),
LlmCacheWriteTokens = toint(ResponseHeaders["x-llm-cache-write-tokens"]),
LlmCacheReadTokens = toint(ResponseHeaders["x-llm-cache-read-tokens"])
| project
CorrelationId,
ApiId,
OperationId,
ResponseCode,
BackendResponseCode,
CallerIpAddress,
EntraOID,
EntraUPN,
ClientType,
LlmInputTokens,
LlmOutputTokens,
LlmCacheWriteTokens,
LlmCacheReadTokens;
ApiManagementGatewayLlmLog
| where TimeGenerated > ago(24h)
| where _ResourceId has "<apim-name>"
| join kind=inner gateway on CorrelationId
| summarize
PromptTokens = sum(PromptTokens),
CompletionTokens = sum(CompletionTokens),
TotalTokens = sum(TotalTokens),
ResponseInputTokens = sum(LlmInputTokens),
ResponseOutputTokens = sum(LlmOutputTokens),
CacheWriteTokens = sum(LlmCacheWriteTokens),
CacheReadTokens = sum(LlmCacheReadTokens),
Requests = count()
by EntraUPN, EntraOID, ClientType, ApiId
| order by TotalTokens desc
That query gives a practical enterprise usage view:
- user
- client type
- APIM API/backend
- request count
- prompt tokens
- completion tokens
- total tokens
- cache write tokens
- cache read tokens
Pricing the usage data
The APIM headers give you request-level usage. Azure Cost Management gives you the billing truth.
That distinction matters. APIM can tell you that Jordan used 10,000 input tokens, 2,000 output tokens, 50,000 cache write tokens, and 200,000 cache read tokens. Cost Management tells you what Azure actually charged for those meter types on that model, deployment, and day.
The pricing model is:
estimated cost =
input_tokens / 1,000,000 * input_rate_per_1M
+ output_tokens / 1,000,000 * output_rate_per_1M
+ cache_write_tokens / 1,000,000 * cache_write_rate_per_1M
+ cache_read_tokens / 1,000,000 * cache_read_rate_per_1M
The response headers map to Cost Management meters like this:
| APIM response header | Cost Management meter |
|---|---|
x-llm-input-tokens |
Input tokens |
x-llm-output-tokens |
Output tokens |
x-llm-cache-write-tokens |
Cache write tokens |
x-llm-cache-read-tokens |
Cache hit/read tokens |
I would not hardcode these rates in APIM. Pull the effective rates from Cost Management, because Azure can expose different meters for regular context, long context, cache writes, cache hits, and model-specific pricing.
The effective rate calculation is:
effective_rate_per_1M_tokens = PreTaxCost / UsageQuantity * 1,000,000
Then apply those rates to the APIM usage totals.
Example APIM-side daily usage query:
let gateway =
ApiManagementGatewayLogs
| where TimeGenerated > ago(30d)
| where _ResourceId has "<apim-name>"
| extend
EntraUPN = tostring(ResponseHeaders["x-entra-upn"]),
InputTokens = tolong(ResponseHeaders["x-llm-input-tokens"]),
OutputTokens = tolong(ResponseHeaders["x-llm-output-tokens"]),
CacheWriteTokens = tolong(ResponseHeaders["x-llm-cache-write-tokens"]),
CacheReadTokens = tolong(ResponseHeaders["x-llm-cache-read-tokens"]);
ApiManagementGatewayLlmLog
| where TimeGenerated > ago(30d)
| where _ResourceId has "<apim-name>"
| join kind=inner gateway on CorrelationId
| summarize
Requests = count(),
InputTokens = sum(InputTokens),
OutputTokens = sum(OutputTokens),
CacheWriteTokens = sum(CacheWriteTokens),
CacheReadTokens = sum(CacheReadTokens)
by bin(TimeGenerated, 1d), ApiId, EntraUPN, ModelName, DeploymentName
| order by TimeGenerated desc, ApiId asc, EntraUPN asc
If you want to model costs directly in Log Analytics, you can load the current rates into a small datatable or external table and calculate estimated cost:
let rates = datatable(ModelName:string, InputPer1M:real, OutputPer1M:real, CacheWritePer1M:real, CacheReadPer1M:real)
[
"claude-opus-4-8", 0.0, 0.0, 0.0, 0.0,
"claude-sonnet-4-6", 0.0, 0.0, 0.0, 0.0,
"claude-haiku-4-5", 0.0, 0.0, 0.0, 0.0
];
let usage =
ApiManagementGatewayLogs
| where TimeGenerated > ago(30d)
| where _ResourceId has "<apim-name>"
| extend
EntraUPN = tostring(ResponseHeaders["x-entra-upn"]),
InputTokens = tolong(ResponseHeaders["x-llm-input-tokens"]),
OutputTokens = tolong(ResponseHeaders["x-llm-output-tokens"]),
CacheWriteTokens = tolong(ResponseHeaders["x-llm-cache-write-tokens"]),
CacheReadTokens = tolong(ResponseHeaders["x-llm-cache-read-tokens"])
| join kind=inner (
ApiManagementGatewayLlmLog
| where TimeGenerated > ago(30d)
| project CorrelationId, ModelName, DeploymentName
) on CorrelationId
| summarize
InputTokens = sum(InputTokens),
OutputTokens = sum(OutputTokens),
CacheWriteTokens = sum(CacheWriteTokens),
CacheReadTokens = sum(CacheReadTokens)
by EntraUPN, ModelName, DeploymentName;
usage
| join kind=leftouter rates on ModelName
| extend
EstimatedInputCost = InputTokens / 1000000.0 * InputPer1M,
EstimatedOutputCost = OutputTokens / 1000000.0 * OutputPer1M,
EstimatedCacheWriteCost = CacheWriteTokens / 1000000.0 * CacheWritePer1M,
EstimatedCacheReadCost = CacheReadTokens / 1000000.0 * CacheReadPer1M
| extend EstimatedTotalCost = EstimatedInputCost + EstimatedOutputCost + EstimatedCacheWriteCost + EstimatedCacheReadCost
| order by EstimatedTotalCost desc
The clean enterprise pattern is:
Cost Management = invoice and meter truth
APIM headers = user, route, request, and cache-token attribution
In practice, I would reconcile them daily:
- Export Cost Management by day, resource, product, meter, and cost.
- Map hidden
Microsoft.SaaS/resources/claude-*rows back to the Foundry account using the Foundry account internal ID embedded in the SaaS resource name. - Calculate effective rates by model and meter.
- Apply those rates to APIM token totals by user, API, model, and day.
- Treat the current day as partial because Cost Management can lag.
Optional Microsoft Graph enrichment
JWT claims are enough for basic attribution. If you need richer reporting, APIM can call Microsoft Graph using managed identity and enrich the logged headers with directory attributes.
Examples:
x-entra-display-name
x-entra-mail
x-entra-user-principal-name
x-entra-job-title
x-entra-department
x-entra-company-name
x-entra-manager-upn
This enables reporting by department, manager, title, or business unit.
Use this intentionally. These attributes are personally identifiable information and will be written to Log Analytics if captured by diagnostics. Only add fields that are genuinely useful for reporting or governance.
Enterprise deployment pattern
Package the client configuration as managed deployment scripts:
- macOS shell scripts for Jamf or Intune.
- Windows PowerShell scripts for Intune, ConfigMgr, GPO, or elevated local install.
- Separate target variants for different APIM routes.
- Codex-only scripts for machines that do not need Claude Code.
- Cleanup logic for stale direct-Foundry settings and API keys.
The deployment scripts should:
- Ensure Azure CLI is installed.
- Install the token helper.
- Write managed Claude Code settings.
- Write Codex provider configuration if needed.
- Remove stale API-key or direct-Foundry environment variables.
- Print validation commands.
The end-user workflow should remain simple:
az login --tenant <tenant-id>
claude
Or:
az login --tenant <tenant-id>
codex
No static model API key is required.
Validation
Validate that Azure CLI can return the expected token:
az account get-access-token \
--tenant <tenant-id> \
--resource https://ai.azure.com \
--query "{expiresOn:expiresOn, tenant:tenant, tokenType:tokenType}" \
-o table
Validate macOS helpers:
/usr/local/bin/claude-azure-ai-token | cut -c1-30
/usr/local/bin/codex-azure-openai-token | cut -c1-30
Validate Windows helpers:
& "$env:ProgramData\ClaudeCode\claude-azure-ai-token.cmd"
& "$env:ProgramData\ClaudeCode\codex-azure-openai-token.cmd"
Validate the configured Windows helper path:
(Get-Content "$env:ProgramFiles\ClaudeCode\managed-settings.json" | ConvertFrom-Json).apiKeyHelper
Expected:
C:\ProgramData\ClaudeCode\claude-azure-ai-token.cmd
Validate the APIM path directly:
TOKEN="$(az account get-access-token \
--tenant <tenant-id> \
--resource https://ai.azure.com \
--query accessToken \
-o tsv)"
curl -sS \
-X POST "https://<apim-name>.azure-api.net/<foundry-api-path>/anthropic/v1/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data '{
"model": "claude-haiku-4-5",
"max_tokens": 16,
"messages": [
{
"role": "user",
"content": "Reply with exactly: gateway-ok"
}
]
}'
Troubleshooting
| Symptom | Likely cause | What to check |
|---|---|---|
401 TokenNotPresent / JWT not present |
Claude Code did not send a bearer token | apiKeyHelper path, helper execution, spaces in Windows paths |
401 with token present |
Token validation failed | aud, iss, tenant ID, accepted audiences |
APIM auth succeeds but backend returns 401 |
Foundry rejected backend auth | APIM managed identity role and stripped client API-key headers |
| Old Foundry resource is used | Stale local environment variables | Remove direct Foundry resource/base URL/API-key settings |
| Direct helper works but Claude fails | Claude did not reload settings/env | Restart terminal and Claude Code |
| Identity headers missing in Log Analytics | Diagnostics not capturing those headers | APIM diagnostic frontend.response.headers |
| Token logs do not join to gateway logs | Missing or mismatched correlation IDs/time range | Query both tables over same time window |
What this pattern gets you
This architecture gives enterprises a clean foundation for AI coding assistant governance:
No static model API keys on developer machines.
No direct backend access from endpoints.
Centralized authentication and policy.
Managed identity from APIM to Foundry.
Per-user and per-route observability.
Quota-ready architecture.
Log Analytics reporting by identity, client, API, and token usage.
The developer workflow stays simple:
az login
claude
or:
az login
codex
The enterprise control plane becomes APIM.
Final thoughts
Using Azure API Management as an AI Gateway gives organizations a practical way to scale AI coding assistants without scattering secrets, policy, and telemetry across individual machines.
Claude Code and Codex can still feel local and natural for developers. But the enterprise gets centralized authentication, routing, governance, quotas, and observability.
The most important principles are:
Do not distribute backend API keys.
Use Entra tokens to identify users at APIM.
Use APIM managed identity to call Foundry.
Strip client credential headers before forwarding.
Log identity metadata and token usage with CorrelationId.
Apply governance at the gateway, not on individual machines.
For organizations standardizing AI-assisted development, APIM can be much more than an API proxy. It can become the AI Gateway where enterprise identity, governance, and reporting come together.
