LiteLLM × Bedrock · 生产实战

把 Bedrock 接成生产级模型网关,
按隔离需求逐层加固

客户想要一个统一的、OpenAI / Anthropic 兼容的入口,背后接 Amazon Bedrock,集中管 key、成本和限流,自己不碰各家 SDK 的差异。把 LiteLLM 摆在中间正合适。难点不在装 LiteLLM,而在于:当客户对网络隔离账号边界的要求越来越高,模型配置要跟着一层层往上加。下面这四层,客户要到哪一层,就配到哪一层。

L1
公网入口
Pod 能上公网,走 Bedrock 公网端点
model + region
▲ 加上私网
L2
本区 VPC Endpoint
Pod 不给公网,流量走本 region VPCE
+ VPCE
▲ 加上跨区
L3
跨区 US Inference Profile
私网访问 us-west-2,走 VPC Peering
+ VPC Peering
▲ 加上跨账号
L4
跨账号统一管理
多个 AWS 账号,一个网关统一发 key、记账
+ AssumeRole
本文沉淀自一套跑在生产上、服务过多个客户的 LiteLLM 代理。文中账号 ID、VPC Endpoint、域名、密钥均已脱敏成占位符(<ACCOUNT_B>vpce-xxxxx),可照此搭建,但不含任何能定位真实资源的信息。
Quick Start

先验证最小链路:一个 Pod 直连公网端点

私网、跨区、跨账号这些隔离要求可以放到后续逐层引入。最快的验证路径,是让一个有公网出口的 Pod 直接调用 Bedrock 公网端点,先把"客户端 → 网关 → Bedrock"整条链路验证通,再按隔离需求往上加固。三步如下。

Quick Start · 最小链路
客户端认虚拟 key ALB超时调到 600s Amazon EKSLiteLLM Pod 公网Pod 有出口 Bedrockglobal.*
  1. 配置一个模型。model_list 里写上 model ID 与 region,凭证由 EKS Pod Identity 自动注入,无需写任何 access key。
  2. 客户端指向网关。应用或 Claude Code 把 base URL 指向网关,使用 LiteLLM 下发的虚拟 key 认证。
  3. 先调大超时。负载均衡器与 LiteLLM 的超时从一开始就设到 600 秒,否则长对话会被中途切断(这是最常见的故障点,详见后文专节)。
最小可用的 model_list
model_list:
  - model_name: claude-sonnet-4-6
    litellm_params:
      model: bedrock/global.anthropic.claude-sonnet-4-6
      aws_region_name: ap-northeast-1
      drop_params: true
首先要配置的一项

把链路上每一层的超时都调到 600 秒并对齐。ALB 的空闲超时默认只有 60 秒,而长对话(扩展思考、agent 多轮)几十秒到几分钟很常见;超过 60 秒没有新数据流过,连接即被断开,而 LiteLLM 仍在正常等待模型返回,排查时容易误判。详见 超时与 LB 一节。

这条最小链路就是后文四层中的第一层。链路验证通过后,先了解 LiteLLM 在这套架构中的定位与整体结构,再回到四层逐层加固。

00
What LiteLLM is here

LiteLLM 在这套架构里的定位

很多人第一次听 LiteLLM,以为它是个装进代码里调模型的 SDK。它确实有 SDK 形态,但这套架构用的是它的另一面:Proxy Server,一个独立部署的服务进程,对外暴露标准的 /v1/chat/completions(OpenAI 格式)和 /v1/messages(Anthropic 格式)两个 HTTP 接口,对内把请求翻译成各家厂商的调用。把它摆在客户和 Bedrock 中间,解决四个很具体的问题。

01
入口统一

客户的应用、Claude Code、各种脚本全指向同一个 endpoint,认同一把 key。换模型、加模型、调路由改的是网关配置,客户端一行代码不用动。

02
凭证收口

Bedrock 的 IAM 凭证、跨账号 AssumeRole 全锁在网关 Pod 里。客户端拿到的只是 LiteLLM 发的虚拟 key,永远看不到 AWS 凭证;撤销某个客户只是删一把虚拟 key。

03
成本和用量看得见

LiteLLM 自带 spend log,每次请求记下走了哪个模型、用了多少 token、折算多少钱,落进数据库。谁用得多、哪个模型成本高,一目了然。

04
模型差异被磨平

Claude 在 Bedrock 上的调用格式、thinking 参数、各种 beta header 跟原厂 API 并不完全一样。网关把这些差异在内部抹平,客户端按标准格式发就行。

Architecture

架构全景

下面这张图是把后面四层配置全叠满之后的样子。链路从客户端进来,经 ALB 到 EKS 上的 LiteLLM Pod,Pod 再按不同模型走三条出口到 Bedrock,账号状态落在 Aurora。

架构全景 · 四层叠满后的完整链路
工作负载 VPC · 本 region us-west-2 VPC 账号 B · <ACCOUNT_B> HTTPS · 虚拟 key spend log 本区私网 跨区 VPC Peering 跨账号 AssumeRole 客户端应用 / Claude Code / 脚本 ALB入站 IP 白名单 Amazon EKS Pod Identity 注入凭证 LiteLLM Podreplica 1 LiteLLM Podreplica 2 Aurora PostgreSQL配置 / 虚拟 key / spend log Bedrock VPCE本区私网入口 Bedrockglobal.* · 本区 Bedrock VPCE Bedrockus.* Bedrock账单各自独立
客户端 网络 · ALB / VPC / VPCE / Peering 容器 · EKS 数据库 · Aurora Bedrock 跨账号 · AssumeRole

几个设计取舍值得说一下。EKS 而非单台 EC2,核心是高可用:生产网关挂了所有客户的访问一起断,不可接受。两个副本滚动更新,升级 LiteLLM 版本零停机;另外留一台 EC2 作冷备,整体故障时接管。ALB 入站锁 IP,因为背后是计费的 Bedrock 调用,安全组开 0.0.0.0/0 等于把高成本入口暴露在公网,虚拟 key 一旦泄露任何人都能发起调用,所以只放行客户已知 IP,这是硬性红线。

还有个常会被问到的点:前面为什么用 ALB,而不像普通 Web 服务那样套一层 CloudFront。两个原因。其一是超时,CloudFront 对源站的响应超时默认 30 秒,能调到的上限也只有 120 秒(还要单独申请配额),而 agent 多轮、扩展思考的对话动辄几分钟,CloudFront 撑不住这么长的连接;ALB 的空闲超时能配到 4000 秒,留得出余量。其二是定位不对,CloudFront 是给可缓存内容做边缘分发的,而网关的流量全是带鉴权、各不相同的 POST 请求,没有可缓存的东西,多套一层只增加一跳延迟和成本,换不来好处。所以网关前面这层用七层负载均衡(ALB)正合适。

Pod Identity 而非 IRSA,两者都是给 Pod 发 AWS 凭证的机制,Pod Identity 配置更简单,且原生支持给会话打 transitive tag,这个特性在第四层跨账号场景会用到。Aurora PostgreSQL Serverless v2 承载 store_model_in_db 和 spend log,两个 Pod 共享同一份记录,按负载自动伸缩,流量不大时成本很低。Pod 规格刻意留小(250m CPU / 1Gi 内存,上限 500m / 2Gi):LiteLLM 代理是 IO 密集型,瓶颈在网络和并发连接数,不在 CPU;securityContext 按最小权限收紧,drop 掉所有 Linux capability、关掉 privilege escalation。

L1
Layer 1 · Public Endpoint最简

Pod 能上公网,走 Bedrock 公网入口

最简单的情况,Pod 有公网出口。这时一个模型只要配 model ID 和 region 两样。凭证由 EKS Pod Identity 自动注入(用工作负载账号自己的权限),LiteLLM 不用配 access key。

model_list · 第一层
model_list:
  - model_name: claude-sonnet-4-6
    litellm_params:
      model: bedrock/global.anthropic.claude-sonnet-4-6
      aws_region_name: ap-northeast-1
      drop_params: true
L1 · Pod 经公网直达 Bedrock
客户端应用 / Claude Code ALB入站 IP 白名单 Amazon EKSLiteLLM Pod 公网Pod 有出口 Bedrockglobal.*

drop_params: true 用于丢弃 Bedrock 不支持的 OpenAI 参数,避免 400。这里要交代 model ID 的形态,因为它是 Bedrock 的规矩,配错直接报错:global.anthropic.claude-sonnet-4-6 这种 global.* 前缀是跨区域推理 profile,请求会被 Bedrock 自动调度到全球有容量的区域,可用性最好,也是 AWS 默认推荐的形态。它必须经 Inference Profile 调用,不能用裸的 base model ID。

到这一层客户已经能用了。但流量走的是 Bedrock 公网入口,很多客户的安全合规过不了这一关。

L2
Layer 2 · Same-Region VPCE+ 私网

Pod 不给公网,走本 region 的 VPC Endpoint

为了加强安全,希望 Pod 完全没有公网访问能力。做法是在 Pod 所在 region 建一个 Bedrock 的 VPC Endpoint(VPCE),让流量全程走 AWS 私有网络。配置上只多一行 aws_bedrock_runtime_endpoint,指向这个 VPCE。

model_list · 第二层
  - model_name: claude-sonnet-4-6
    litellm_params:
      model: bedrock/global.anthropic.claude-sonnet-4-6
      aws_region_name: ap-northeast-1
      aws_bedrock_runtime_endpoint: https://vpce-xxxxx.bedrock-runtime.ap-northeast-1.vpce.amazonaws.com
      drop_params: true
L2 · 本区 VPCE,全程私网
工作负载 VPC · 本 region · Pod 无公网路由 私网 私有骨干 ALB LiteLLM PodEKS · 无公网 Bedrock VPCEPrivate DNS · 443 Bedrockglobal.*

AWS 侧要提前准备三件事:在工作负载 VPC 里创建 com.amazonaws.<region>.bedrock-runtime 的 interface endpoint 并开启 Private DNS;VPCE 安全组放行来自 Pod 子网的 443 入站;Pod 子网去掉公网路由(或者直接放在私有子网里),确保访问 Bedrock 只走 VPCE。

这一层 VPCE 和 Pod 在同一个 VPC,Private DNS 生效,其实用默认服务域名也能命中 VPCE。配置里显式写 VPCE 域名主要是让流量走向一目了然,也为第三层跨 VPC 的场景做好铺垫。

L3
Layer 3 · Cross-Region US Profile+ 跨区私网

跨 region 用 US Inference Profile,且全程私网

AWS 默认推荐 global.*,但有些客户希望把推理入口固定在美国区域,用 US Inference Profileus.* 前缀),同时还是不想走公网。原因是 global.* 是全球调度,偶尔会有后端响应延迟波动,把入口锁到 us.*(比如 us-west-2)能拿到更稳的表现。但 us.* 的调用入口必须落在美国区域,不能从亚太的 endpoint 发。

要做到"跨 region 访问 us-west-2 的 Bedrock、又全程私网",做法是在工作负载 VPC(比如东京)和一个 us-west-2 的 VPC 之间建 跨区域 VPC Peering,在 us-west-2 那侧放 Bedrock 的 VPCE,流量经 Peering 私网过去。

L3 · 跨区 US Profile,经 VPC Peering 全程私网
东京 VPC · 10.2.0.0/16 us-west-2 VPC · 10.1.0.0/16 跨区域 VPC Peering · 私网 ALB LiteLLM Podaws_region=us-west-2 Bedrock VPCEvpce-…us-west-2(特有域名) Bedrockus.* · US Inference Profile
model_list · 第三层
  - model_name: claude-opus-4-8-us
    litellm_params:
      model: bedrock/us.anthropic.claude-opus-4-8
      aws_region_name: us-west-2
      aws_bedrock_runtime_endpoint: https://vpce-usw2-xxxxx.bedrock-runtime.us-west-2.vpce.amazonaws.com
      drop_params: true

网络层有三件事必须做对:

延迟方面需要提前告知客户:跨太平洋的 Peering 比本 region VPCE 多约 100~150ms,主要体现在首 token 延迟。流式输出建立连接后,后续 token 受影响不大。如果想保留灵活性,可以把 us.* 配成 global.* 的 fallback,平时走 global,需要时自动切换。

L4
Layer 4 · Cross-Account+ 跨账号

多个 AWS 账号访问 Bedrock,用一个 LiteLLM 统一管

客户如果有多个 AWS 账号都要用 Bedrock,但希望用同一个网关统一管理、统一发 key、统一记账,做法是跨账号 AssumeRole:网关的 Pod 先 assume 到目标账号的一个角色,再用临时凭证调那个账号的 Bedrock。这样每个账号的 Bedrock 账单各自独立,便于对账。配置上加一行 aws_role_name,指向目标账号的跨账号角色。

model_list · 第四层
  - model_name: claude-sonnet-4-6-acct-b
    litellm_params:
      model: bedrock/global.anthropic.claude-sonnet-4-6
      aws_region_name: ap-northeast-1
      aws_role_name: arn:aws:iam::<ACCOUNT_B>:role/LiteLLM-Bedrock-CrossAccount-Role
      aws_session_name: bedrock-session
      aws_bedrock_runtime_endpoint: https://vpce-xxxxx.bedrock-runtime.ap-northeast-1.vpce.amazonaws.com
      drop_params: true
L4 · 跨账号 AssumeRole 调用链
工作负载账号 · EKS 账号 B · <ACCOUNT_B> sts:AssumeRole + TagSession InvokeModel LiteLLM PodPod Identity Pod Role本账号 IAM Cross-Account Role信任工作负载账号 Pod Role Bedrock账单各账号独立

IAM 配置要覆盖两个账号。工作负载账号的 Pod Role 权限策略加上对目标角色的 sts:AssumeRole,信任策略允许 pods.eks.amazonaws.com 来 assume(这是 EKS Pod Identity 的标准信任主体)。目标账号里建一个跨账号角色,信任策略允许工作负载账号的 Pod Role 来 assume,权限策略给 Bedrock 调用。

容易漏的细节

两边的策略都要带上 sts:TagSession,跟 sts:AssumeRole 成对。EKS Pod Identity 注入凭证时会自动附带一组 transitive session tag,Pod 拿这套凭证去 AssumeRole 时,这些 tag 会跟着传递。目标账号的信任策略如果只允许了 sts:AssumeRole 而没有 sts:TagSession,调用会直接返回 AccessDenied。

如果客户连 STS 认证也要求私网(不许走公网做 AssumeRole),在 Pod 所在 region 再建一个 STS 的 VPC Endpoint(开 Private DNS)即可。AssumeRole 是 Pod 在本地发起的,本 region 的 STS VPCE 天然命中,不需要在对端 region 建。

+
Stacking & Special IDs

四层叠起来,和两种特殊 model ID

上面四层是正交的,可以按客户需求自由组合:既要跨 region US profile、又要跨账号、又要全私网,就是第二、三、四层叠满,也就是架构全景那张图。除了 global.*us.*,还有两种 model ID 形态会用到。

裸 ID
Bedrock 上的开源权重模型

如 GLM、Kimi 等,它们不支持跨区推理 profile,所以反过来必须用裸 model ID、不加 global./us. 前缀,并经 bedrock/converse/ 路径调用。连带地,Pod 的 IAM policy 里要为这些模型逐个加 foundation-model ARN,Claude 系列的通配 ARN 覆盖不到它们。

AIP
Application Inference Profile

需要精细成本归属时(比如对接 AWS MAP 抵扣)用它给调用打可追踪标签,本地凭证调用即可。限制是:AIP 只能包裹某个区域里真实存在的 base model,不能包裹 global.* 跨区 profile,所以能不能做要看目标区域有没有该模型的区域 base model。

几个值得复用的全局设置

litellm_settings 等
litellm_settings:
  drop_params: true        # 丢弃 Bedrock 不认的参数,避免 400
  request_timeout: 600     # 长推理留足时间
  num_retries: 2           # 瞬时失败自动重试
  fallbacks:               # 模型级降级链:某模型失败时改打备选
    - claude-opus-4-6: [claude-opus-4-5, claude-sonnet-4-5]
    - claude-sonnet-4-6: [claude-sonnet-4-5]
  context_window_fallbacks: # 上下文超窗时切到大窗口变体
    - claude-sonnet-4-5: [claude-4-5-sonnet-1M]

general_settings:
  store_model_in_db: true
  store_prompts_in_spend_logs: true

fallbackscontext_window_fallbacks 是两件事:前者是某模型调用失败(限流、报错)时自动改打备选模型,保可用性;后者是请求上下文超过当前模型窗口上限时,自动切到大窗口变体(比如从 200K 的 Sonnet 切到 1M 的 Sonnet)。前者应对调用失败,后者应对上下文超窗。

Client Setup · Claude Code

本地用 Claude Code,怎么指向这个网关

很多客户的开发者本地就用 Claude Code,它默认直连 Anthropic 官方 API。要让它改走自建网关,关键有两步:把它指向网关地址,再用一个 apiKeyHelper 脚本把虚拟 key 喂进去。这里有个容易踩的坑:只在 env 里设一个 ANTHROPIC_AUTH_TOKEN 往往跑不通。原因是静态 token 只会作为 Authorization 一个 header 发出,而 LiteLLM 的虚拟 key 校验读的是 x-api-keyapiKeyHelper 输出的 key 会同时带上 AuthorizationX-Api-Key 两个 header,这才是配置能跑通的关键。Claude Code 用的是 /v1/messages(Anthropic 格式)这个入口。

~/.claude/settings.json
{
  "apiKeyHelper": "~/.claude/litellm-key.sh",
  "env": {
    "ANTHROPIC_BASE_URL":            "https://<你的 LiteLLM 网关地址>",
    "ANTHROPIC_DEFAULT_OPUS_MODEL":   "claude-opus-4-8",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL":  "claude-haiku-4-5"
  }
}
~/.claude/litellm-key.sh · 记得 chmod +x
#!/bin/bash
# 最简:直接回显虚拟 key
echo "<你的 LiteLLM 虚拟 key>"

三类设置各管一件事。apiKeyHelper 指向一个输出虚拟 key 的脚本,Claude Code 启动时执行它拿到 key,并同时塞进 AuthorizationX-Api-Key 两个 header,少了它光设 env token 经常鉴权失败。ANTHROPIC_BASE_URL 把全部请求转到网关 ALB,不再直连 Anthropic;填的是虚拟 key 而非 AWS 凭证,客户端始终看不到底层的 Bedrock 权限。后面三个 ANTHROPIC_DEFAULT_*_MODEL 把 Claude Code 内置的 opus / sonnet / haiku 三档,分别映射到 LiteLLM model_list 里的 model_name,值要字面一致,否则切档时网关找不到对应模型。

配好之后

会话里 /model sonnet/model opus 即时切档,客户端一行代码不用改。key 会轮换的场景,把脚本换成从 vault 取 key,再用 CLAUDE_CODE_API_KEY_HELPER_TTL_MS 设刷新间隔即可。验证链路通不通,直接对网关的 /v1/messages 发一个 curl 看是否返回 200。

Thinking Parameters

Thinking 参数按模型代际区分

Claude 的 extended thinking(扩展思考)在 Bedrock 上,参数格式随模型代际不同,配错会出现"看起来开了思考却没思考"的情况。

写法Opus 4.7 / 4.8Opus 4.6 / Sonnet 4.6说明
thinking.type: adaptive✓ 推荐模型按任务复杂度自己决定思考多少
output_config.effortlow/medium/high/xhigh/max;必须在 output_config 里,不能塞进 thinking,否则 ValidationException。xhigh 仅 4.7/4.8,已 GA
thinking.type: enabled + budget_tokens已废弃仍可用budget 已废弃;4.7/4.8 推荐改用 adaptive
一个版本相关的注意点

在较老的 LiteLLM 版本上,给 Opus 4.7/4.8 发废弃的 {type: "enabled", budget_tokens: N},会返回 200 + 纯文本但没有 thinking block,且不报错。这个行为在 LiteLLM v1.88.1 已经修复。稳妥起见,给 4.7/4.8 一律用 adaptive。

响应侧还有个细节:Opus 4.8/4.7 默认是 omitted summary 模式,thinking block 的 text 字段是空的,完整推理加密在 signature 字段里供多轮续传。客户端读到空的 thinking 文本属于正常,多轮回传时把 block 原样带上即可。

Timeouts & the Load Balancer

超时对齐:负载均衡器的默认值必须改大

这是上生产后最容易被忽视、又最容易翻车的一处。LiteLLM 自己有 request_timeout,但真正先掐断对话的,往往不是它,而是它前面那层负载均衡器。客户的对话一旦跑长(扩展思考、长输出、agent 多轮),几十秒很常见,而 ALB 的 idle timeout 默认只有 60 秒。超过 60 秒没有新数据流过,ALB 就把连接断开,客户端看到的是请求中断或 504,但 LiteLLM 那边其实还在正常等模型返回。这个现象排查起来很迷惑,因为 LiteLLM 日志里看不到错误。

解决办法是把链路上每一层的超时都调到能覆盖最长请求,并且彼此对齐。ALB 通过 ingress 注解把 idle timeout 从默认 60 秒调到 600 秒(或按你预期的最长对话估);LiteLLM 的 request_timeout 设成同一个值。两层一致,行为才可预测,否则永远是最短的那层先触发,配了 600 也照样 60 秒断。这也是网关前面用 ALB 而不是 CloudFront 的原因之一:CloudFront 对源站的响应超时最高只能到 120 秒,覆盖不了长对话。

如果网关前面不是 ALB 而是自建 Nginx,道理一样:Nginx 的 proxy_read_timeout / proxy_send_timeout 默认也是 60 秒,必须一起调大,否则 Nginx 先断。流式输出并不能绕开这点,因为 idle timeout 算的是"两次数据之间的间隔":模型在首 token 之前如果思考很久(扩展思考下很常见),这段静默就可能撞上 idle timeout。所以首 token 之前的等待,才是最需要留余量的地方。

配置项默认建议
ALBidle_timeout.timeout_seconds(ingress 注解)60s600s
Nginx(自建)proxy_read_timeout / proxy_send_timeout60s600s
LiteLLMrequest_timeout(配置文件)600s
Cost · Observability

成本追踪与可观测性

成本追踪

LiteLLM 的 spend log 默认按内置成本价目表(cost map)折算每次请求的费用。新模型刚出时 cost map 往往还没收录,spend log 会算出 0 成本。临时手段是在 model 配置上手动挂 input_cost_per_token / output_cost_per_token 自定义单价,等 LiteLLM 版本更新补上价目后(比如 v1.88.1 补了 Opus 4.8 的计价),自定义价可以移除,保留也无影响。需要把成本归到特定项目或对接 AWS MAP 抵扣时,用前面提到的 Application Inference Profile 给调用打标签。

可观测性

EKS 上装 CloudWatch Observability add-on,自带两个 DaemonSet:CloudWatch Agent 收 Pod/节点的 CPU、内存、网络(进 Container Insights),Fluent Bit 把容器的 stdout/stderr 转发进 CloudWatch Logs(保留 30 天)。LiteLLM 侧开两个环境变量就有结构化日志:LITELLM_LOG=INFO(每次请求记模型、路由决策、HTTP 状态、token 用量)和 LITELLM_DETAILED_TIMING=true(记各阶段耗时)。排查疑难时临时调成 LITELLM_LOG=DEBUG 看完整请求/响应体,DEBUG 有性能开销,排查完调回 INFO。日志落在 /aws/containerinsights/<cluster>/application 日志组,用 Logs Insights 查询最方便。

Pre-Production Checklist

上生产前的核对清单

各层要查的点不一样:先过一遍每套部署都适用的通用项,再按客户用到的层往下补。

通用 · 每套部署都要查

L2 起 · 走本区私网时追加

L3 追加 · 跨区私网时

L4 追加 · 跨账号时

这套方案的价值不在"LiteLLM 怎么装",那部分有官方文档;而在于把网络隔离和账号边界的需求拆成四层递进的配置,客户要到哪一层,就配到哪一层,每一层该动的 LiteLLM 参数和 AWS 资源都对应清楚。