这篇是我照着 Microsoft 社区里一篇实战文章跑通之后整理的笔记,原文作者是 MuraliKumanduri,2026 年 6 月发布,原文在这里。我把它跑通并按自己的理解重讲一遍,方便以后复用。

先说结论:这是一套能上生产的模式,给跑在 Microsoft Foundry 里的 Claude 模型前面套一层用 Entra 保护的 LLM 网关,做到按开发者认证、限流、配额和成本追踪,而且任何一台笔记本上都不落模型密钥。全套实现大概两小时,只想搭个最小试点的话半小时够了。

我想解决的问题

我想让团队里的工程师都用上 Claude Code,但不想给每个人发 Anthropic 的 API key。直接把 Claude Code 指向 Anthropic、或者干脆指向 Foundry,对于超过几个人的团队都会带来三个麻烦:

  1. 密钥和账单会失控。 用一把共享 key,就没法按人分账,轮换起来是场灾难;一人一把 key,采购和离职回收又是没完没了的活。
  2. 没有节流。 Claude Code 很吃 token,它读文件、做规划、在长循环里反复改代码。一个跑飞的会话,或者一个用得太猛的团队,就能刷出一张吓人的账单,而开发者和模型之间没有任何拦截。
  3. 看不见花销。 财务想知道每个团队花了多少钱,安全想知道谁在调什么。一把裸 key,这两样都给不了。

我的解法是在中间架一台每个请求都必须流经的网关:它知道开发者是谁(Entra ID),管着他能用多少(APIM 的 GenAI 策略),还记着他用了什么(Azure Monitor)。Claude Code 本身就支持这种网关配置,正好对上。

具体拆开来说,这套方案是这样的:

  • Claude 模型跑在 Microsoft Foundry 里,走你的 Azure 订阅计费,不需要 Anthropic 合同,也不需要它的 key。
  • Azure API Management(APIM)架在前面当 LLM 网关:用 Entra ID 认证每个开发者,按人限流、按人做 token 配额,再把每个人的用量指标打出来做分账回收。
  • Foundry 单独放在它自己的 Azure 订阅里,APIM 用一把 Foundry API key 去访问它,跨订阅的 RBAC 一点都不用碰。
  • 开发者手上只有短时效的 Entra token,Foundry 的 key 永远不出 APIM。

下面的每一步都是照着 Claude Code 的 LLM 网关要求和 APIM 的 GenAI 网关策略来的。所有命令行我都用 Windows 的 PowerShell 写。

整体架构

开发者笔记本上的 Claude Code 拿一个 Entra ID bearer token 去认证 APIM;APIM 校验 token,套上按人的 token 和请求限制,换成 Foundry 的 API key,再把 Anthropic Messages 请求转发给另一个订阅里的 Claude;每个人的 token 用量都打到 Application Insights。

请求路径长这样:

Developer laptop  (Claude Code CLI / VS Code)
   |   Authorization: Bearer <Entra access token for the APIM app>
   v
Azure API Management   (the LLM gateway)              [Subscription A]
   |  1. validate-jwt            confirm Entra identity, audience, app role
   |  2. extract oid             per-user counter key
   |  3. llm-token-limit         per-user tokens/min + monthly token quota
   |  4. rate-limit-by-key       per-user requests/min
   |  5. strip Authorization; set api-key from secret named value
   |  6. llm-emit-token-metric   per-user usage to App Insights
   v   (forwards Anthropic Messages format; anthropic-* headers preserved)
Microsoft Foundry  https://{resource}.services.ai.azure.com/anthropic/v1/messages
   v                                                    [Subscription B]
Claude deployments   (Sonnet 4.6 / Haiku 4.5 / Opus 4.6)

这里最关键的一点是:面向开发者的认证和面向后端的认证互不相干。开发者永远用自己的 Entra ID 在网关这层证明身份;网关拿什么去认证 Foundry 是另一件事,有两条都能走的路。

网关怎么认证 Foundry

下面两个选项跟开发者侧的 Entra ID 认证都没关系,而且不管 Foundry 跟 APIM 是不是同一个订阅都能用。托管标识唯一的硬约束是两个资源得在同一个 Entra 租户里。

选项 A — Foundry API key 选项 B — 托管标识
APIM 怎么认证 从密文命名值取 api-key 头 用 APIM 托管标识拿 Entra token,放进 Authorization 头
搭建 读一次 key,存进 APIM 开 APIM 的标识,在 Foundry 上授 Cognitive Services User
同订阅 可用 可用
跨订阅 可用,没有 RBAC 跨边界 可用,同租户下角色分配能跨订阅
跨租户 可用 不支持,得用 key
要轮换的共享密钥 没有
适合 起步最快;跨租户;只有 key 的环境 生产;彻底去掉共享密钥

我这份笔记从头到尾走 key 方案,同时在第 3、4 部分把托管标识的替换点原地标出来。选一条就行,不用两条都做。

这套设计到底解决了什么

目标 怎么做到的
开发者用 Claude Code 但没有 Anthropic 账单和 key Claude 跑在 Microsoft Foundry,走你的 Azure 订阅计费
Foundry 可以放在另一个订阅 APIM 只靠 URL + API key 访问 Foundry,没有跨订阅 RBAC
每个开发者都以自己的身份认证 Entra ID token 在 APIM 网关处校验
按人限流和配额 rate-limit-by-key + llm-token-limit,都以 Entra 的 oid claim 为键
按人追踪用量和成本 llm-emit-token-metric → Application Insights / Log Analytics
笔记本上不落 Foundry key Foundry key 只活在 APIM 里,开发者只拿短时效的 Entra token

前置条件

  • 两个 Azure 订阅,都是按量付费。订阅 A 放 APIM,订阅 B 放 Foundry。(Foundry 的 Claude 不能跑在免费、试用、赞助或 CSP 订阅上。)
  • 一个 Microsoft Foundry 资源(订阅 B),放在 Claude 可用的区域,目前是 East US 2Sweden Central,并且已经创建好 Claude 部署,在 Keys and Endpoint 下至少有一把 API key。
  • 一个 API Management 实例(订阅 A)。试点用 Developer SKU 就够;生产和 VNet 集成用 Standard v2Premium
  • 读订阅 B 里 Foundry key 的权限、APIM 实例的 contributor 权限,以及注册 Entra 应用的能力。
  • 开发者用 Windows,装好 PowerShell(自带 5.1 或 7)、Azure CLIwinget install Microsoft.AzureCLI)和 Claude Code CLI
  • 试点成本:APIM Developer SKU 每月约 50 美元,再加上按 token 消耗算的 Claude 用量。

选项 A(key)没有跨订阅的角色分配,唯一的跨订阅动作是读一次 Foundry key(第 3 部分),这个从 Foundry 门户也能做。选项 B(托管标识)有一个跨订阅的角色分配(Cognitive Services User),只要 APIM 和 Foundry 在同一个 Entra 租户就支持。

第 1 部分 — 在 Foundry 里部署 Claude(订阅 B)

  1. 在 Foundry 门户打开 Model catalog,搜 Claude,把 Claude Code 用到的模型部署出来。部署名要跟模型 ID 对齐,这样网关就能原样透传 model 字段:
    角色 建议的部署名
    主力(日常写代码) claude-sonnet-4-6
    快速(读文件、小改动、后台任务) claude-haiku-4-5
    扩展思考(可选) claude-opus-4-6
  2. 锁版本 — 选一个具体版本,不要选 auto-update to latest。不锁的话,一次新模型发布就能同时把所有开发者搞崩。
  3. 在资源的 Keys and Endpoint 页复制 endpoint 和两把 API key 里的一把。Anthropic 的 endpoint 基址是:
https://{resource}.services.ai.azure.com/anthropic

这里有个坑要特别提醒:Foundry 的 Claude endpoint 是 Anthropic 那套接口/anthropic/v1/messages),不是 OpenAI 那套(/openai/deployments/.../chat/completions?api-version=...)。在 APIM 里建 API 时,别套 OpenAI 的策略模板,别加 api-version 查询参数,也别改写成 /openai/... 路径。这几样里任何一样都会引出大家常撞的 “not supported” 或 “resource not found” 报错。我第一次就是手贱套了 OpenAI 模板,排查了半天。

检查点: 到这一步你的 Claude 已经在 Foundry 里部署好了。继续第 2 部分之前,先去 Foundry 门户确认部署没问题。

第 2 部分 — Entra ID 应用注册(面向开发者)

这个注册在订阅 A 的租户里。它定义开发者 token 签发时对应的 audience,也就是 APIM 要校验的东西。它跟 Foundry 放在哪没关系。

  1. App registrations → New registration → 起个名,比如 Claude Code Gateway
  2. Expose an API → 设 Application ID URI,比如 api://claude-code-gateway。加一个 scope access_as_user(管理员 + 用户同意)。
  3. (可选,做分级用) App roles → 加 Claude.StandardClaude.Premium 这类角色。在 Enterprise applications → 这个应用 → Users and groups 下把开发者或组分配进去。
  4. 记下 Application (client) IDApplication ID URI 和你的 Tenant ID

开发者向这个应用的 audience 请求 token;APIM 校验 aud = api://claude-code-gateway

第 3 部分 — 建 APIM 的 API 和 Foundry 后端(订阅 A)

3.1 选项 A — 把 Foundry API key 存进 APIM

先从订阅 B 的 Foundry 里读 key(用 --subscription,这样不用切当前上下文):

# Read a Foundry key from Subscription B
$FOUNDRY_KEY = az cognitiveservices account keys list `
  --name <foundry-resource> `
  --resource-group <foundry-rg> `
  --subscription <SUBSCRIPTION_B_ID> `
  --query key1 -o tsv

然后把它作为 密文命名值存进 APIM(订阅 A)。策略里用 {{foundry-api-key}} 引用:

# Create a secret named value in APIM holding the Foundry key
az apim nv create -g <apim-rg> --service-name <apim-name> `
  --named-value-id foundry-api-key `
  --display-name foundry-api-key `
  --value "$FOUNDRY_KEY" `
  --secret true

想更稳一点的话:别把裸 key 放 APIM,放进 Key Vault,再建一个 Key Vault 支持的命名值,轮换就集中在一个地方管。APIM 需要一个对那个 vault 有 Get/List 密文权限的托管标识,不过 vault 跟 APIM 都在订阅 A,所以这还是不算跨订阅的角色分配。

3.2 选项 B — 给 APIM 一个托管标识

不想管共享 key 的话,跳过 3.1,给 APIM 一个 Foundry 信任的标识。这个在同订阅跨订阅下都能用,只要两个资源在同一个 Entra 租户。

# Enable a system-assigned managed identity on APIM (Subscription A)
az apim update -g <apim-rg> --name <apim-name> `
  --set identity.type=SystemAssigned

# Get the identity's principal (object) ID
$APIM_MI = az apim show -g <apim-rg> --name <apim-name> `
  --query identity.principalId -o tsv

# Get the Foundry resource ID (Subscription B)
$FOUNDRY_ID = az cognitiveservices account show `
  --name <foundry-resource> --resource-group <foundry-rg> `
  --subscription <SUBSCRIPTION_B_ID> `
  --query id -o tsv

# Grant Cognitive Services User on the Foundry resource (works cross-subscription)
az role assignment create `
  --assignee-object-id $APIM_MI `
  --assignee-principal-type ServicePrincipal `
  --role "Cognitive Services User" `
  --scope $FOUNDRY_ID

Cognitive Services User 角色(a97b65f3-24c7-4388-baec-2e87135dc908)给的是调模型的数据面权限,不带密钥管理权。角色分配要几分钟才生效。用用户分配的标识也行,把它挂到 APIM 上,然后在策略里引用它的 client ID(第 4 部分选项 B)。走这条路就没有 foundry-api-key 命名值要建、要轮换了。

3.3 建后端和 API

# Named backend pointing at the Foundry Anthropic endpoint (Subscription B URL)
az apim backend create -g <apim-rg> --service-name <apim-name> `
  --backend-id foundry-claude `
  --url "https://<foundry-resource>.services.ai.azure.com/anthropic" `
  --protocol http

# API with NO path suffix so callers hit /v1/messages at the gateway root
az apim api create -g <apim-rg> --service-name <apim-name> `
  --api-id claude-anthropic --display-name "Claude (Foundry)" `
  --path="" --protocols https `
  --service-url "https://<foundry-resource>.services.ai.azure.com/anthropic"

PowerShell 加空字符串:写 --path=""(用 = 连起来),不要写成 --path "" 两个 token。PowerShell 会在 az 包装器看到之前就把裸的 "" 吃掉,于是 CLI 报 argument --path: expected one argument。用 = 的写法能保住它是单个 token(--path=),az 读作空字符串。你从 PowerShell 往 az 传任何空字符串值,都得用这招。

把 Claude Code 会调的操作加上(一个通配符全覆盖):

  • POST /v1/messages
  • POST /v1/messages/count_tokens
  • GET /v1/models (只有开了网关模型发现才需要,见 5.3)

az apim 没法应用 XML 策略。第 4 部分的策略请通过门户(APIs → Claude (Foundry) → Inbound processing → policy editor)或 Bicep/ARM 来加。

第 4 部分 — APIM 策略(认证 + 限流 + 计量)

在 API 层加这段策略。替换掉里面的 tenant ID 和 audience。下面这份是 key 方案(选项 A)版本,它的第 6 步删掉开发者的 Authorization 头,改用密文命名值里的 api-key 头。走**托管标识(选项 B)**的话,把第 6 步换成紧跟策略后面那段,其余步骤完全一样。

<policies>
  <inbound>
    <base />
     <!-- On the client, Bearer token is generated and passed as x-api-key -->
    <set-header name="Authorization" exists-action="skip">
            <value>@("Bearer " + context.Request.Headers.GetValueOrDefault("x-api-key",""))</value>
    </set-header>
    <!-- 1. Validate the developer's Entra ID token -->
    <validate-jwt header-name="Authorization"
                  failed-validation-httpcode="401"
                  failed-validation-error-message="Unauthorized: invalid or missing Entra token.">
      <openid-config url="https://login.microsoftonline.com/{{tenant-id}}/v2.0/.well-known/openid-configuration" />
      <audiences>
        <audience>{{gateway-audience}}</audience>
      </audiences>
      <issuers>
        <issuer>https://login.microsoftonline.com/{{tenant-id}}/v2.0</issuer>
        <!-- This is needed as  Claude Code's Foundry Mode is looking for scope as https://cognitiveservices.azure.com/.default and audience cannot be changed to APIM Audience (api://...) -->
        <issuer>https://sts.windows.net/{{tenant-id}}/</issuer>
      </issuers>
      <required-claims>
        <claim name="roles" match="any">
          <value>Claude.Standard</value>
          <value>Claude.Premium</value>
        </claim>
      </required-claims>
    </validate-jwt>

        <!-- 2. Per-developer key from the stable object id -->
        <set-variable name="callerId" value="@{
        var jwt = context.Request.Headers
            .GetValueOrDefault("Authorization","").Split(' ').Last().AsJwt();
        return jwt.Claims.GetValueOrDefault("oid", "unknown");
    }" />
        <!-- 3. Tier from app role -->
        <set-variable name="tier" value="@{
        var jwt = context.Request.Headers
            .GetValueOrDefault("Authorization","").Split(' ').Last().AsJwt();
        return jwt.Claims.GetValueOrDefault("roles","").Contains("Claude.Premium") ? "premium" : "standard";
    }" />
        <set-variable name="modelName" value="@{
      var body = context.Request.Body.As<JObject>(preserveContent: true);
      return body?["model"]?.ToString() ?? "unknown";
    }" />
        <!-- 4. Token-based throttle per developer (controls LLM cost) -->
        <choose>
            <when condition="@(((string)context.Variables["tier"]) == "premium")">
                <llm-token-limit counter-key="@((string)context.Variables["callerId"])" tokens-per-minute="200000" estimate-prompt-tokens="true" remaining-tokens-header-name="x-tokens-remaining" token-quota="20000000" token-quota-period="Monthly" />
                <rate-limit-by-key calls="300" renewal-period="60" counter-key="@((string)context.Variables["callerId"])" retry-after-header-name="retry-after" remaining-calls-header-name="x-ratelimit-remaining" />
            </when>
            <otherwise>
                <llm-token-limit counter-key="@((string)context.Variables["callerId"])" tokens-per-minute="50000" estimate-prompt-tokens="true" remaining-tokens-header-name="x-tokens-remaining" token-quota="5000000" token-quota-period="Monthly" />
                <rate-limit-by-key calls="100" renewal-period="60" counter-key="@((string)context.Variables["callerId"])" retry-after-header-name="retry-after" remaining-calls-header-name="x-ratelimit-remaining" />
            </otherwise>
        </choose>
         <!-- 5. Request-rate throttle per developer -->
        <llm-emit-token-metric namespace="claudecode">
            <dimension name="UserId" value="@((string)context.Variables["callerId"])" />
            <dimension name="Tier" value="@((string)context.Variables["tier"])" />
            <dimension name="Model" value="@((string)context.Variables["modelName"])" />
        </llm-emit-token-metric>
        <!-- 6. Authenticate to Foundry with its API key (secret named value) -->
        <!-- Strip the developer's Entra token so Foundry never sees it -->
        <set-header name="Authorization" exists-action="delete" />
        <set-header name="x-api-key" exists-action="override">
            <value>{{foundry-api-key}}</value>
        </set-header>
        <set-backend-service backend-id="foundry-claude" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

选项 B — 用托管标识认证 Foundry

如果你选了托管标识那条路(3.2),把上面的第 6 步换成下面这段。它不注入 api-key,而是让 APIM 给自己的标识拿一个 Entra token,当作 Authorization bearer 转发出去。token 校验、限流、计量都不变。

  <!-- 6 (Option B). Authenticate to Foundry with APIM's managed identity -->
    <!-- Replace the developer's token with an MI token scoped to AI Services -->
    <authentication-managed-identity
        resource="https://cognitiveservices.azure.com"
        output-token-variable-name="msi-token" />
    <set-header name="Authorization" exists-action="override">
      <value>@("Bearer " + (string)context.Variables["msi-token"])</value>
    </set-header>

    <set-backend-service backend-id="foundry-claude" />

Azure AI Services / Foundry 的 token audience 是 https://cognitiveservices.azure.com。用用户分配标识的话,给 authentication-managed-identity 元素加上 client-id="<uami-client-id>"。这条路没有 api-key 命名值、没有密钥要轮换,这也正是它更适合生产的原因。

关于这份策略,有几点值得单独拎出来说:

  • 转发前删掉开发者的 Authorization 头(第 6 步)很重要:那个 Entra token 只是给 APIM 的。Foundry 只该收到 api-key 头。
  • {{tenant-id}}{{gateway-audience}}{{foundry-api-key}} 都是 APIM 命名值。把 foundry-api-key 标为密文,前两个用普通命名值就行。
  • llm-token-limitllm-emit-token-metric 是 APIM 的 GenAI 网关策略,它们看得懂 Anthropic/OpenAI 的消息格式,会解析 token 用量,所以你计的是 token 而不只是请求数。对吃 token 的 Claude Code 来说,这才是对的成本抓手。
  • 这些计数器是按区域按网关的。用多区域 APIM 时,限制是按每个区域各自算的。

第 5 部分 — 在开发者机器上配 Claude Code、Claude Desktop 和 Cowork

开发者把 Claude Code、Claude Desktop、Cowork 指向 APIM(Anthropic Messages 网关模式),用自己的 Entra token 认证。后端认证的替换对客户端是透明的。

5.1 Entra token 助手脚本(按人、自动刷新)

%USERPROFILE%\.claude\get-claude-gateway-token.ps1

# Returns a short-lived Entra access token for the APIM gateway audience.
# Supports CLAUDE_HELPER_CONTEXT for Claude Desktop Cowork/Code silent refresh.

$context = $env:CLAUDE_HELPER_CONTEXT

# For non-interactive contexts, try silent token acquisition only.
# If it fails, exit non-zero so Cowork prompts the user instead of blocking.
if ($context -and $context -ne 'interactive' -and $context -ne 'setup-test') {
    try {
        $token = az account get-access-token `
            --resource "api://claude-code-gateway" `
            --query accessToken -o tsv 2>$null
        if ($LASTEXITCODE -ne 0 -or -not $token) {
            Write-Error "Silent token refresh failed (context: $context)"
            exit 1
        }
        Write-Output $token
        exit 0
    } catch {
        Write-Error "Silent token refresh failed: $_"
        exit 1
    }
}

# Interactive or setup-test context: allow az CLI to prompt if needed.
$token = az account get-access-token `
    --resource "api://claude-code-gateway" `
    --query accessToken -o tsv

if ($LASTEXITCODE -ne 0 -or -not $token) {
    Write-Error "Token acquisition failed. Run 'az login' first."
    exit 1
}

Write-Output $token
exit 0

PowerShell 脚本不需要 chmod。如果执行策略拦了这个助手,给你的用户放行一次本地脚本:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

5.2 Claude Code 设置(%USERPROFILE%\.claude\settings.json

.claude 文件夹下的 settings.json 里配好下面这些环境变量,就能对所有 Claude Code 会话(VS Code、终端 CLI、JetBrains 等)生效:

{
  "env": {
       "ANTHROPIC_BASE_URL": "https://<apim-name>azure-api.net",
       "ANTHROPIC_MODEL": "claude-opus-4-8",
       "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8",
       "CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "600000"
 },
  "apiKeyHelper": "powershell -NoProfile -ExecutionPolicy Bypass -File C:\\Users\\<you>\\.claude\\get-claude-gateway-token.ps1"
}

JSON 里反斜杠得写两个,所以是 C:\\Users\\...。用 PowerShell 7 的话把 powershell 换成 pwsh

  • apiKeyHelper 的输出会作为 Authorization(和 X-Api-Key)头发出去,由 APIM 的 validate-jwt 校验。开发者从头到尾没碰过 Foundry key。
  • CLAUDE_CODE_API_KEY_HELPER_TTL_MS=3600000 会每小时刷新一次 token(Entra access token 大概活 60 到 90 分钟)。
  • 把三个 ANTHROPIC_DEFAULT_*_MODEL ID 钉死,能保证 Claude Code 发出去的模型名跟你的 Foundry 部署名一致,网关就原样透传 model。
  • Sonnet、Haiku 这些别的 Anthropic 模型也能配。默认用哪个模型由 ANTHROPIC_MODEL 决定。

配好之后,开发者在项目目录里跑 claude 就行。

5.3 可选 — 模型发现

想让网关模型出现在 /model 选择器里,就在 API 上暴露 GET /v1/models,并设 CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1(Claude Code v2.1.129+)。只有以 claudeanthropic 开头的 ID 才会显示。

5.4 VS Code 扩展

.claude 文件夹里的 settings.json 会同时管住 VS Code 扩展和 Claude Code CLI。

5.5 Claude Desktop 和 Cowork

装上 Claude Desktop 和 Cowork。在 Help → Troubleshooting → Enable Developer Mode 里打开开发者模式,露出 Developer 菜单。然后去 Developer → Configure third-party inference 打开配置。

在连接那一节,Inference Provider 选 Gateway,指向 APIM endpoint。可以填你暴露出来的模型列表。你得用凭据助手脚本,指向上面 5.1 的那个 PowerShell 脚本,用它来做 Entra ID 认证。管理员自己在机器上试通之后,可以按 Export and deploy via MDM 把这套配置分发出去。

第 6 部分 — 限流和用量追踪的设计

按人做键。 所有东西都以 Entra 的 oid claim 为键,它稳定且每人唯一,不像 email 或 upn 那样会变。服务账号或 CI 的话,改用 appid 做键。

限流分两层。一层是 llm-token-limit,管 tokens/min 加一个每月 token 配额,这是真正的成本控制。另一层是 rate-limit-by-key,管 requests/min,防的是跑飞的循环。

分级靠 Entra 的 app role(Claude.Standard / Claude.Premium)驱动,从 JWT 里读,不用另外去管 APIM 的订阅。

用量追踪llm-emit-token-metric 流进 Application Insights,带 UserId、Tier、Model 三个维度。下面是一条按人算月度 token 花销的 Log Analytics 查询示例:

customMetrics
| where name == "Total Tokens" and customDimensions.namespace == "claudecode"
| extend UserId = tostring(customDimensions.UserId), Model = tostring(customDimensions.Model)
| summarize Tokens = sum(valueSum) by UserId, Model, bin(timestamp, 1d)
| order by Tokens desc

Foundry 不返回 Anthropic 那套标准的限流响应头,所以限制的管理和观测要走 APIM(上面那些头)和 Azure Monitor,别指望上游的头。

第 7 部分 — 测试和验证

# 1. Get a token as a developer
$TOKEN = az account get-access-token --resource "api://claude-code-gateway" --query accessToken -o tsv

# 2. Call the gateway directly in Anthropic Messages format
$body = @{
  model      = "claude-sonnet-4-6"
  max_tokens = 64
  messages   = @(@{ role = "user"; content = "Say hello in one word." })
} | ConvertTo-Json

Invoke-RestMethod -Method Post `
  -Uri "https://<apim-name>.azure-api.net/v1/messages" `
  -Headers @{
    "Authorization"     = "Bearer $TOKEN"
    "anthropic-version" = "2023-06-01"
    "content-type"      = "application/json"
  } `
  -Body $body

Invoke-RestMethod 会返回解析好的 body,但把响应头藏起来了。想看 x-tokens-remaining / x-ratelimit-remaining,用 Invoke-WebRequest-ResponseHeadersVariable resp(然后读 $resp),或者用 curl.exe -i(真正的 curl,不是 PowerShell 那个 curl 别名)。

验证清单:

  • 没 token / token 过期 → validate-jwt 返回 401(信任限流之前先确认这个)。
  • 有效 token → 200 带一段 Claude 补全;响应里带 x-tokens-remaining / x-ratelimit-remaining
  • 有效开发者 token 却从 Foundry 收到 401 → api-key 命名值不对,或者没注入(看排错那节)。
  • 超限 → 429retry-after
  • App Insights → customMetrics 里能看到按 UserId 分维度的 token 计数。
  • 然后在项目目录里把 claude 端到端跑一遍。

第 8 部分 — 运维和加固

  • 密钥轮换(选项 A)。 Foundry 给你两把 key。轮换时先把 foundry-api-key 命名值改成 key2,再重新生成 key1,零停机。用 Key Vault 支持的命名值能把这事变成一处改动。
  • 生产上优先托管标识(选项 B)。 如果你是从 key 起步的,切到托管标识(3.2 和 4 的选项 B),把共享密钥彻底去掉。因为 Cognitive Services User 角色分配在同租户下能跨订阅,跨订阅这套拓扑不会挡住这次升级,而且开发者那边没有任何变化,因为他们那半张契约永远是"以自己的身份认证网关"。
  • 私有网络。 把 APIM 放进 internal VNet 模式,通过 Private Endpoint 访问 Foundry;关掉 Foundry 的公网访问,让网关成为唯一入口。跨订阅的私有端点是支持的。
  • 弹性。 把 Claude 部署到两个区域,用 APIM 负载均衡的后端池,遇到 429 和 5xx 时重试。
  • 成本护栏。 把按人的 llm-token-limit 配额配上一个 Azure Budget,并对订阅 B 里的 Foundry 资源设告警。

排错

现象 原因 / 修法
Foundry 返回 404 resource not found 后端 URL 或路径错了,或者套了 OpenAI 式改写。后端必须以 /anthropic 结尾,调用方打的是 /v1/messages。去掉任何 /openai/... 改写和 api-version 查询参数。
Foundry 返回 401(开发者 token 有效)— 选项 A api-key 头缺失或不对,或者 foundry-api-key 命名值没按预期存好。确认命名值,并确认策略删掉了开发者 Authorization 头、设了 api-key。
Foundry 返回 401 / 403选项 B(托管标识) 角色分配缺失或还没生效,或者 token audience 错了。确认 APIM 的标识在 Foundry 资源上有 Cognitive Services User,等几分钟,并确保策略请求的是 resource="https://cognitiveservices.azure.com"。用户分配标识的话,确认 client-id 设了。
托管标识同订阅能用、跨订阅不行 两个资源在不同的 Entra 租户。跨租户托管标识不支持,改用 API key(选项 A)。
带了 token 却在网关处 401 aud 或 issuer 不匹配。确认 token 的 aud = api://claude-code-gateway,并且你用的是 v2.0 的 OIDC 配置和 issuer。
Foundry 返回 403 key 属于另一个 Foundry 资源,或者该资源关了 key 认证。从 Keys and Endpoint 重新复制一把 key,或重新开启本地/key 认证。
Claude Code 功能变弱 网关把 anthropic-beta / anthropic-version 头剥掉了。确保这两个头都透传过去。
模型不可用 Claude Code 的模型 ID 跟 Foundry 部署名对不上。把名字对齐,或者在策略里改写 body 的 model 字段。
客户端 ChainedTokenCredential authentication failed 开发者没登录。跑 az login,让助手有个能用的 Azure 凭据。

收尾

跑通之后回头看,一个下午的搭建换来的是一条每个 Claude Code 请求都必须流经的网关:Entra ID 证明开发者是谁,APIM 的 GenAI 策略管住每个人能花多少,Application Insights 告诉你 token 到底花在哪了。APIM 到 Foundry 这一跳你按需要选:一把只活在 APIM 里的 Foundry API key(起步最快,跨租户也行),或者一个完全没有共享密钥的托管标识(生产该有的姿势)。不管哪种,Claude 都能待在它自己的订阅里,而开发者手上最敏感的东西也就是一个短时效的 Entra token。

我个人比较喜欢这套设计的一点是升级路径很干净:从 key 起步,先把它挪进 Key Vault,再升到托管标识把密钥彻底去掉,最后把整条路放进私有网络。这几步没有一步会打扰到开发者,因为他们那半张契约(以自己的身份认证网关)从头到尾都没变过。

所有命令行都针对 Windows 上的 PowerShell 5.1 或 7。文中的模型 ID 和 Foundry 区域反映的是写作时的可用情况,具体以 Foundry 模型目录当前的选项为准。

延伸阅读: