把 Bedrock 接成生产级模型网关,
按隔离需求逐层加固
客户想要一个统一的、OpenAI / Anthropic 兼容的入口,背后接 Amazon Bedrock,集中管 key、成本和限流,自己不碰各家 SDK 的差异。把 LiteLLM 摆在中间正合适。难点不在装 LiteLLM,而在于:当客户对网络隔离和账号边界的要求越来越高,模型配置要跟着一层层往上加。下面这四层,客户要到哪一层,就配到哪一层。
先验证最小链路:一个 Pod 直连公网端点
私网、跨区、跨账号这些隔离要求可以放到后续逐层引入。最快的验证路径,是让一个有公网出口的 Pod 直接调用 Bedrock 公网端点,先把"客户端 → 网关 → Bedrock"整条链路验证通,再按隔离需求往上加固。三步如下。
- 配置一个模型。
model_list里写上 model ID 与 region,凭证由 EKS Pod Identity 自动注入,无需写任何 access key。 - 客户端指向网关。应用或 Claude Code 把 base URL 指向网关,使用 LiteLLM 下发的虚拟 key 认证。
- 先调大超时。负载均衡器与 LiteLLM 的超时从一开始就设到 600 秒,否则长对话会被中途切断(这是最常见的故障点,详见后文专节)。
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 在这套架构中的定位与整体结构,再回到四层逐层加固。
LiteLLM 在这套架构里的定位
很多人第一次听 LiteLLM,以为它是个装进代码里调模型的 SDK。它确实有 SDK 形态,但这套架构用的是它的另一面:Proxy Server,一个独立部署的服务进程,对外暴露标准的 /v1/chat/completions(OpenAI 格式)和 /v1/messages(Anthropic 格式)两个 HTTP 接口,对内把请求翻译成各家厂商的调用。把它摆在客户和 Bedrock 中间,解决四个很具体的问题。
客户的应用、Claude Code、各种脚本全指向同一个 endpoint,认同一把 key。换模型、加模型、调路由改的是网关配置,客户端一行代码不用动。
Bedrock 的 IAM 凭证、跨账号 AssumeRole 全锁在网关 Pod 里。客户端拿到的只是 LiteLLM 发的虚拟 key,永远看不到 AWS 凭证;撤销某个客户只是删一把虚拟 key。
LiteLLM 自带 spend log,每次请求记下走了哪个模型、用了多少 token、折算多少钱,落进数据库。谁用得多、哪个模型成本高,一目了然。
Claude 在 Bedrock 上的调用格式、thinking 参数、各种 beta header 跟原厂 API 并不完全一样。网关把这些差异在内部抹平,客户端按标准格式发就行。
架构全景
下面这张图是把后面四层配置全叠满之后的样子。链路从客户端进来,经 ALB 到 EKS 上的 LiteLLM Pod,Pod 再按不同模型走三条出口到 Bedrock,账号状态落在 Aurora。
几个设计取舍值得说一下。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。
Pod 能上公网,走 Bedrock 公网入口
最简单的情况,Pod 有公网出口。这时一个模型只要配 model ID 和 region 两样。凭证由 EKS Pod Identity 自动注入(用工作负载账号自己的权限),LiteLLM 不用配 access key。
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
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 公网入口,很多客户的安全合规过不了这一关。
Pod 不给公网,走本 region 的 VPC Endpoint
为了加强安全,希望 Pod 完全没有公网访问能力。做法是在 Pod 所在 region 建一个 Bedrock 的 VPC Endpoint(VPCE),让流量全程走 AWS 私有网络。配置上只多一行 aws_bedrock_runtime_endpoint,指向这个 VPCE。
- 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
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 的场景做好铺垫。
跨 region 用 US Inference Profile,且全程私网
AWS 默认推荐 global.*,但有些客户希望把推理入口固定在美国区域,用 US Inference Profile(us.* 前缀),同时还是不想走公网。原因是 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 私网过去。
- 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
网络层有三件事必须做对:
- endpoint 必须显式写 VPCE 的特有域名(
vpce-开头的那个)。VPCE 的 Private DNS 只在创建它的那个 VPC 内生效,跨 Peering 不传播。东京 Pod 解析默认的bedrock-runtime.us-west-2.amazonaws.com会拿到公网 IP,不会走 VPCE。只有写死这个特有域名,才能解析到 VPCE 的私有 IP、正确走 Peering。 aws_region_name必须跟 VPCE 所在区域一致(这里是us-west-2)。AWS SDK 的请求签名按这个 region 算,对不上签名失败。- 两侧的路由表和安全组要配齐。东京和 us-west-2 两边,Pod 所在子网的路由表都要加一条指向对端 CIDR、target 是 Peering 连接的路由;us-west-2 的 Bedrock VPCE 安全组放行来自东京 VPC CIDR 的 443 入站。
延迟方面需要提前告知客户:跨太平洋的 Peering 比本 region VPCE 多约 100~150ms,主要体现在首 token 延迟。流式输出建立连接后,后续 token 受影响不大。如果想保留灵活性,可以把 us.* 配成 global.* 的 fallback,平时走 global,需要时自动切换。
多个 AWS 账号访问 Bedrock,用一个 LiteLLM 统一管
客户如果有多个 AWS 账号都要用 Bedrock,但希望用同一个网关统一管理、统一发 key、统一记账,做法是跨账号 AssumeRole:网关的 Pod 先 assume 到目标账号的一个角色,再用临时凭证调那个账号的 Bedrock。这样每个账号的 Bedrock 账单各自独立,便于对账。配置上加一行 aws_role_name,指向目标账号的跨账号角色。
- 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
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 建。
四层叠起来,和两种特殊 model ID
上面四层是正交的,可以按客户需求自由组合:既要跨 region US profile、又要跨账号、又要全私网,就是第二、三、四层叠满,也就是架构全景那张图。除了 global.* 和 us.*,还有两种 model ID 形态会用到。
如 GLM、Kimi 等,它们不支持跨区推理 profile,所以反过来必须用裸 model ID、不加 global./us. 前缀,并经 bedrock/converse/ 路径调用。连带地,Pod 的 IAM policy 里要为这些模型逐个加 foundation-model ARN,Claude 系列的通配 ARN 覆盖不到它们。
需要精细成本归属时(比如对接 AWS MAP 抵扣)用它给调用打可追踪标签,本地凭证调用即可。限制是:AIP 只能包裹某个区域里真实存在的 base model,不能包裹 global.* 跨区 profile,所以能不能做要看目标区域有没有该模型的区域 base model。
几个值得复用的全局设置
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
fallbacks 和 context_window_fallbacks 是两件事:前者是某模型调用失败(限流、报错)时自动改打备选模型,保可用性;后者是请求上下文超过当前模型窗口上限时,自动切到大窗口变体(比如从 200K 的 Sonnet 切到 1M 的 Sonnet)。前者应对调用失败,后者应对上下文超窗。
本地用 Claude Code,怎么指向这个网关
很多客户的开发者本地就用 Claude Code,它默认直连 Anthropic 官方 API。要让它改走自建网关,关键有两步:把它指向网关地址,再用一个 apiKeyHelper 脚本把虚拟 key 喂进去。这里有个容易踩的坑:只在 env 里设一个 ANTHROPIC_AUTH_TOKEN 往往跑不通。原因是静态 token 只会作为 Authorization 一个 header 发出,而 LiteLLM 的虚拟 key 校验读的是 x-api-key;apiKeyHelper 输出的 key 会同时带上 Authorization 和 X-Api-Key 两个 header,这才是配置能跑通的关键。Claude Code 用的是 /v1/messages(Anthropic 格式)这个入口。
{
"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"
}
}
#!/bin/bash # 最简:直接回显虚拟 key echo "<你的 LiteLLM 虚拟 key>"
三类设置各管一件事。apiKeyHelper 指向一个输出虚拟 key 的脚本,Claude Code 启动时执行它拿到 key,并同时塞进 Authorization 和 X-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 参数按模型代际区分
Claude 的 extended thinking(扩展思考)在 Bedrock 上,参数格式随模型代际不同,配错会出现"看起来开了思考却没思考"的情况。
| 写法 | Opus 4.7 / 4.8 | Opus 4.6 / Sonnet 4.6 | 说明 |
|---|---|---|---|
| thinking.type: adaptive | ✓ 推荐 | ✓ | 模型按任务复杂度自己决定思考多少 |
| output_config.effort | ✓ | ✓ | low/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 原样带上即可。
超时对齐:负载均衡器的默认值必须改大
这是上生产后最容易被忽视、又最容易翻车的一处。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 之前的等待,才是最需要留余量的地方。
| 层 | 配置项 | 默认 | 建议 |
|---|---|---|---|
| ALB | idle_timeout.timeout_seconds(ingress 注解) | 60s | 600s |
| Nginx(自建) | proxy_read_timeout / proxy_send_timeout | 60s | 600s |
| LiteLLM | request_timeout(配置文件) | — | 600s |
成本追踪与可观测性
成本追踪
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 查询最方便。
上生产前的核对清单
各层要查的点不一样:先过一遍每套部署都适用的通用项,再按客户用到的层往下补。
通用 · 每套部署都要查
- ALB 安全组绝不开
0.0.0.0/0入站,只放行客户已知 IP。 - 链路上每层超时都调大并对齐,ALB / Nginx 默认 60s,长对话必断,建议统一调到 600s。
- 客户端只拿虚拟 key,AWS 凭证锁在网关 Pod 里,绝不下发给客户端。
- 给 Opus 4.7/4.8 一律用
thinking: adaptive,别发废弃的budget_tokens。 - 要用 server-side tool(如 web search)的主模型,关掉
drop_params,否则工具定义被清掉。 - 开源权重模型用裸 ID、走
bedrock/converse/,并在 IAM 里逐个加 ARN。 - 配 AIP 成本追踪前,确认目标区域有该模型的区域 base model,AIP 包不了
global.*。
L2 起 · 走本区私网时追加
- 本 region 建 Bedrock VPCE 并开 Private DNS,Pod 子网去掉公网路由。
- VPCE 安全组放行来自 Pod 子网的 443 入站。
L3 追加 · 跨区私网时
- endpoint 必须显式写 VPCE 特有域名,Private DNS 不跨 VPC 传播,写默认域名会解析到公网 IP。
aws_region_name跟 VPCE 所在区域一致,否则 SDK 签名失败。- 两侧路由表加指向对端 CIDR 的 Peering 路由、安全组放行对端 VPC CIDR。
L4 追加 · 跨账号时
- 两边的 IAM 策略都带上
sts:TagSession,与sts:AssumeRole成对,否则 AccessDenied。 - 目标账号的跨账号角色信任工作负载账号的 Pod Role,权限策略给到 Bedrock 调用。
这套方案的价值不在"LiteLLM 怎么装",那部分有官方文档;而在于把网络隔离和账号边界的需求拆成四层递进的配置,客户要到哪一层,就配到哪一层,每一层该动的 LiteLLM 参数和 AWS 资源都对应清楚。