乐于分享
好东西不私藏

Claude Code 源码揭秘:为什么它能无感切换 AWS、Google、Azure

Claude Code 源码揭秘:为什么它能无感切换 AWS、Google、Azure

💡 阅读前记得关注+星标,及时获取更新推送

「Claude Code 源码揭秘」系列的第十五篇,上一篇《Claude Code 源码揭秘:CLAUDE.md,一个 Markdown 文件如何驯服 AI》,说的是怎么用 Markdown 文件定制 AI 行为。但不管规则怎么配,最终请求还是要发到云端。问题来了:你用的是哪个云?

你可能没注意过,Claude Code 不只能连 Anthropic 官方 API。

把 AWS_REGION 和 AWS_ACCESS_KEY_ID 配好,它就自动切换到 AWS Bedrock。把 ANTHROPIC_VERTEX_PROJECT_ID 配好,它就走 Google Vertex AI。甚至从 2.0.45 版本开始,还支持了 Microsoft Azure 的 Foundry。不需要改代码,不需要换命令行参数,环境变量一设,底层就变了。

有人可能会问:Bedrock 和 Vertex AI 上跑的不还是 Claude 模型吗?为什么不直连 Anthropic?

模型确实是同一个,但企业访问它的路径完全不同。很多大企业跟 AWS 或 Google 签了年框合约,走云厂商账单可以抵扣承诺消费。更关键的是数据合规——数据不出自己的云环境,IAM 权限、VPC 网络隔离、审计日志都走现有体系。在大公司新增一个供应商要走几个月的采购审批,走已有云厂商能省掉这些流程。所以多云适配不是技术炫技,是实实在在的企业刚需。

翻源码才发现,这套多云适配不是简单地 if-else 判断,而是一套完整的抽象层设计——工厂模式、策略模式、模型映射、认证适配、错误转换,每一层都有讲究。

工厂模式:入口只有一个

整个多云适配的核心是 createClient() 这个工厂函数:

export function createClient(config?: ProviderConfig): Anthropic {  const providerConfig = config || detectProvider();  switch (providerConfig.type) {    case 'bedrock':      return createBedrockClient(providerConfig);    case 'vertex':      return createVertexClient(providerConfig);    case 'foundry':      return createFoundryClient(providerConfig);    default:      return new Anthropic({        apiKey: providerConfig.apiKey,      });  }}

上层代码不管你用的是哪个云厂商,统一调 createClient() 就行。返回的都是 Anthropic 客户端接口,方法一样,参数一样,行为一样。

这就像 USB 接口——不管你插的是鼠标、键盘还是 U 盘,接口都是一样的,设备自己去适配。

环境变量驱动的自动检测

最让我欣赏的是 detectProvider() 这个设计:

function detectProvider(): ProviderConfig {  // 优先级 1:显式指定用 Bedrock  if (process.env.CLAUDE_CODE_USE_BEDROCK === 'true') {    return detectBedrockConfig();  }  // 优先级 2:显式指定用 Vertex  if (process.env.CLAUDE_CODE_USE_VERTEX === 'true') {    return detectVertexConfig();  }  // 优先级 3:有 Bedrock 环境变量就用 Bedrock  if (process.env.AWS_BEDROCK_MODEL ||      (process.env.AWS_REGION && process.env.AWS_ACCESS_KEY_ID)) {    return detectBedrockConfig();  }  // 优先级 4:有 Vertex 环境变量就用 Vertex  if (process.env.ANTHROPIC_VERTEX_PROJECT_ID ||      process.env.GOOGLE_APPLICATION_CREDENTIALS) {    return detectVertexConfig();  }  // 优先级 5:降级到 Anthropic 直连  return {    type: 'anthropic',    apiKey: process.env.ANTHROPIC_API_KEY,  };}

这个设计很巧妙——环境变量本身就是意图声明。你设置了 AWS 相关的变量,说明你想用 AWS;设置了 Google 相关的变量,说明你想用 Google。不需要额外的配置文件或命令行参数。

画个图更清楚:

我之前做的那个多模型网关,用户必须在配置文件里写明「我要用哪个供应商」。Claude Code 这种自动检测更智能——你有什么凭证,我就用什么供应商。

认证方式:四朵云,四种方言

这是多云适配最复杂的部分。每家云厂商的认证方式都不一样,就像去不同国家过海关,每个国家要的证件都不同:

  • • Anthropic:标准的 API Key,Authorization: Bearer <key>
  • • AWS Bedrock:AWS Signature V4 签名,每个请求都要签
  • • Google Vertex:JWT + OAuth2,要用 Service Account 换 Access Token
  • • Azure Foundry:Azure AD 认证,走 Microsoft 的身份体系

Anthropic 最简单,直接把 API Key 塞 header 就完事了。

Bedrock 麻烦得多。AWS 不用 API Key,用 IAM 凭证。每个请求都要算一遍签名:

export function signAWSRequest(  method: string,  url: string,  body: string,  credentials: {    accessKeyId: string;    secretAccessKey: string;    sessionToken?: string;    region: string;    service: string;  }): Record<string, string> {  const date = new Date();  const dateStamp = formatDate(date, 'YYYYMMDD');  const amzDate = formatDate(date, 'YYYYMMDDTHHmmssZ');  // 1. 创建规范请求  const canonicalRequest = [    method,    parsedUrl.pathname,    parsedUrl.searchParams.toString(),    canonicalHeaders,    signedHeaders,    hashBody(body),  ].join('\n');  // 2. 创建待签字符串  const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;  const stringToSign = [    'AWS4-HMAC-SHA256',    amzDate,    credentialScope,    hash(canonicalRequest),  ].join('\n');  // 3. 计算签名  const signingKey = getSigningKey(secretAccessKey, dateStamp, region, service);  const signature = hmacSha256(signingKey, stringToSign);  // 4. 构建授权头  return {    'Authorization': `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,    'X-Amz-Date': amzDate,    'X-Amz-Security-Token': sessionToken,  // 临时凭证需要这个  };}

AWS Signature V4 这套签名流程,做过 AWS 集成的应该都踩过坑。日期格式、header 排序、编码规则,任何一个细节错了,就是 InvalidSignatureException

Claude Code 有个巧妙的 fallback 机制。如果装了官方的 @anthropic-ai/bedrock-sdk,就用 SDK(性能更好);否则降级到手动签名:

try {  const AnthropicBedrock = require('@anthropic-ai/bedrock-sdk').default;  return new AnthropicBedrock({    awsAccessKey: config.accessKeyId,    awsSecretKey: config.secretAccessKey,    awsSessionToken: config.sessionToken,    awsRegion: config.region,  });} catch {  console.warn('[Bedrock] 官方 SDK 未找到,降级到手动签名');  return createManualBedrockClient(config);}

这种「优雅降级」的设计,让 Claude Code 不依赖特定的 SDK 版本也能工作。

Vertex 的认证又是另一套逻辑。Google 用 Service Account 认证,需要用私钥签名一个 JWT,然后拿 JWT 去换 Access Token:

private async fetchServiceAccountToken(  credentials: GoogleServiceAccount): Promise<AccessToken> {  const now = Math.floor(Date.now() / 1000);  // 1. 构建 JWT Claims  const claim = {    iss: credentials.client_email,    scope: 'https://www.googleapis.com/auth/cloud-platform',    aud: credentials.token_uri,    iat: now,    exp: now + 3600,  // 1 小时有效  };  // 2. 用私钥签名 JWT  const jwt = this.signJWT(    { alg: 'RS256', typ: 'JWT' },    claim,    credentials.private_key  );  // 3. 换取 Access Token  const response = await fetch(credentials.token_uri, {    method: 'POST',    body: new URLSearchParams({      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',      assertion: jwt,    }),  });  return response.json();}

Access Token 是有过期时间的,Claude Code 还做了自动刷新:

private scheduleTokenRefresh(token: AccessToken): void {  // 在过期前 5 分钟刷新  const refreshTime = (token.expires_in - 300) * 1000;  this.tokenRefreshTimer = setTimeout(async () => {    this.cachedToken = null;    await this.getAccessToken();  // 重新获取  }, refreshTime);}

这个细节很重要。我之前做的项目没有自动刷新,用户会遇到「下午 4 点后 API 就不工作了」的问题——因为 token 在中午过期了。

模型 ID 映射:统一方言

各家云厂商对同一个模型的命名完全不同——同一个 sonnet,换个云就换个马甲:

export const MODEL_MAPPING: Record<ProviderType, Record<string, string>> = {  anthropic: {    'claude-3-5-sonnet': 'claude-3-5-sonnet-20241022',    'claude-3-opus': 'claude-3-opus-20240229',    'claude-3-haiku': 'claude-3-haiku-20240307',  },  bedrock: {    'claude-3-5-sonnet': 'anthropic.claude-3-5-sonnet-20241022-v2:0',    'claude-3-opus': 'anthropic.claude-3-opus-20240229-v1:0',    'claude-3-haiku': 'anthropic.claude-3-haiku-20240307-v1:0',  },  vertex: {    'claude-3-5-sonnet': 'claude-3-5-sonnet-v2@20241022',    'claude-3-opus': 'claude-3-opus@20240229',    'claude-3-haiku': 'claude-3-haiku@20240307',  },};

上层代码只需要说「我要 sonnet」,底层自动翻译成各厂商的 ID。用户不需要记「Bedrock 里 sonnet 的 ID 是 anthropic.claude-3-5-sonnet-20241022-v2:0」这种细节。

export function getModelForProvider(  modelName: string,  providerType: ProviderType): string {  const mapping = MODEL_MAPPING[providerType];  // 先查映射表  if (mapping[modelName]) {    return mapping[modelName];  }  // 没有映射就原样返回(可能是用户指定的完整 ID)  return modelName;}

Bedrock 还有个特殊的 ARN 解析:

export function parseBedrockModelArn(input: string): BedrockModelArn | null {  // ARN 格式:arn:aws:bedrock:{region}:{accountId}:{type}/{modelId}  const arnPattern = /^arn:aws:bedrock:([^:]+):([^:]*):([^/]+)\/(.+)$/;  const match = input.match(arnPattern);  if (!match) return null;  const [, region, accountId, resourceType, modelId] = match;  return {    region,    accountId,    resourceType,    modelId,    isFoundationModel: resourceType === 'foundation-model',    isProvisionedModel: resourceType === 'provisioned-model',    isInferenceProfile: resourceType.includes('inference-profile'),  };}

Bedrock 有基础模型、预配置模型、跨区域推理三种类型,ARN 格式都不一样。这个解析函数把复杂性隐藏起来了。

端点格式适配

各家厂商的 API 端点格式也是各说各话:

Anthropic:https://api.anthropic.com/v1/messagesBedrock:https://bedrock-runtime.{region}.amazonaws.com/model/{modelId}/invokeVertex:https://{region}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{region}/publishers/anthropic/models/{modelId}:streamRawPredict

Claude Code 用 getEndpoint() 系列函数封装这些差异:

// Bedrock 端点function getBedrockEndpoint(region: string, modelId: string): string {  return `https://bedrock-runtime.${region}.amazonaws.com/model/${modelId}/invoke`;}// Vertex 端点function getVertexEndpoint(  region: string,  projectId: string,  modelId: string): string {  return `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/anthropic/models/${modelId}:streamRawPredict`;}

上层代码完全不需要知道这些细节。

错误处理的统一抽象

AWS 的错误类型有二十多种,Claude Code 做了友好化转换:

export function handleBedrockError(error: any): string {  const errorMessage = error.message || '';  if (errorMessage.includes('InvalidSignatureException')) {    return '签名验证失败,请检查 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY 是否正确';  }  if (errorMessage.includes('AccessDeniedException')) {    return '权限不足,你的 IAM 用户/角色需要 bedrock:InvokeModel 权限';  }  if (errorMessage.includes('ResourceNotFoundException')) {    return '模型不存在,请确认该模型在你的 AWS 区域已开通';  }  if (errorMessage.includes('ThrottlingException')) {    return '请求被限流,请稍后重试或申请提高配额';  }  if (errorMessage.includes('ServiceQuotaExceededException')) {    return '超出服务配额限制,请在 AWS 控制台申请提高配额';  }  if (errorMessage.includes('ValidationException')) {    return '请求参数验证失败,请检查 max_tokens 等参数是否符合模型限制';  }  // ... 还有十几种错误  return `Bedrock 调用失败: ${errorMessage}`;}

用户看到的是人话,不是 AWS 的原始错误码。「你的 IAM 用户需要 bedrock:InvokeModel 权限」比 AccessDeniedException 有用多了。

配置验证和诊断

Claude Code 还提供了配置验证工具:

export function validateProviderConfig(config: ProviderConfig) {  const errors: string[] = [];  const warnings: string[] = [];  if (config.type === 'bedrock') {    // 检查必填字段    if (!config.region) {      errors.push('AWS_REGION 未设置');    }    if (!config.accessKeyId) {      errors.push('AWS_ACCESS_KEY_ID 未设置');    }    if (!config.secretAccessKey) {      errors.push('AWS_SECRET_ACCESS_KEY 未设置');    }    // 检查格式    if (config.region && !isValidAwsRegion(config.region)) {      warnings.push(`区域 ${config.region} 不在已知列表中,请确认拼写`);    }    // 检查凭证长度    if (config.accessKeyId && config.accessKeyId.length < 16) {      errors.push('AWS_ACCESS_KEY_ID 长度不正确');    }  }  return {    valid: errors.length === 0,    errors,    warnings,  };}

还有个 CLI 诊断命令:

$ claude provider diagnoseProvider Detection: bedrockEnvironment Variables:  ✓ AWS_REGION: us-east-1  ✓ AWS_ACCESS_KEY_ID: AKIA...  ✓ AWS_SECRET_ACCESS_KEY: ****  ○ AWS_SESSION_TOKEN: not setModel Configuration:  ✓ Model ID: anthropic.claude-3-5-sonnet-20241022-v2:0  ✓ Region supports model: yesValidation:  ✓ Configuration is validConnection Test:  ✓ Successfully connected to Bedrock

这种诊断工具在企业环境里特别有用。出问题的时候,跑一下诊断命令,大部分配置问题都能定位出来。

LLM Gateway:企业级的中间代理层

前面说的都是 Claude Code 直连云厂商的场景。但在真实的企业环境里,还有一种更常见的部署方式——在中间加一层 LLM Gateway。

LLM Gateway 就是夹在 Claude Code 和云厂商之间的代理层——统一认证、追踪用量、控成本、审计日志,一个入口管住所有开发者。几十号人的团队,总不能每人自己管一套 Key 吧。

Claude Code 官方文档专门写了 Gateway 接入方式。以社区里流行的 LiteLLM 为例:

# 直连 Anthropic 格式(推荐)export ANTHROPIC_BASE_URL=https://litellm-server:4000# 走 Bedrock 透传export ANTHROPIC_BEDROCK_BASE_URL=https://litellm-server:4000/bedrockexport CLAUDE_CODE_SKIP_BEDROCK_AUTH=1export CLAUDE_CODE_USE_BEDROCK=1# 走 Vertex 透传export ANTHROPIC_VERTEX_BASE_URL=https://litellm-server:4000/vertex_ai/v1export CLAUDE_CODE_SKIP_VERTEX_AUTH=1export CLAUDE_CODE_USE_VERTEX=1

注意那两个 SKIP_AUTH 的标志位。设了之后,Claude Code 不再自己处理认证,而是把认证交给 Gateway 去做。这个设计很聪明——Gateway 已经统一管理了密钥和凭证,Claude Code 就不需要再操心签名和 token 这些事了。

有个细节容易踩坑:Gateway 必须正确转发 anthropic-betaanthropic-version 这些 header,它们控制着 Claude Code 的功能开关,丢了就会莫名其妙地少一些高级功能。

我在实际项目里见过不少企业用 Gateway 做多租户隔离——每个团队一个 Key,后台按部门分摊成本,月底出账单一目了然。

完整的请求流程

把整个多云适配流程串起来:

这套设计教会我什么

看完这套多云适配,有几个设计决策值得抄作业:

别硬编码云厂商逻辑 — 工厂 + 策略模式,新增一个厂商只加一个 createXxxClient(),不动已有代码。Foundry 就是这么无痛接入的。

环境变量即意图 — 有什么凭证用什么供应商,零配置自动切换,比写配置文件优雅得多。

认证要有纵深 — 基础认证只是及格线,SSO、临时凭证、自动刷新、Gateway 透传才是企业级。

翻译层是粘合剂 — 模型 ID 映射、端点格式转换、错误信息翻译,这三层”同声传译”让上层代码完全不用关心底层是谁。

我之前做企业级产品就吃过这个亏。一开始只支持一个厂商,后来客户要求加一个,改得满头大汗。要是一开始就用这种抽象层设计,后面扩展会轻松很多。

下一篇聊集成系统。Claude Code 是怎么和 GitHub、VS Code、Chrome 浏览器打通的?这些集成背后的协议和机制是什么?

本文基于 Claude Code 源码分析,主要文件:src/providers/index.tssrc/providers/vertex.tssrc/providers/bedrock.tssrc/providers/foundry.ts

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Claude Code 源码揭秘:为什么它能无感切换 AWS、Google、Azure

评论 抢沙发

7 + 3 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮