数据截至 (上游 commit a5ac26f27989)
多供应商 LLM 网关(@cline/llms)
本章讲什么: Cline 要同时支持 Anthropic、OpenAI、Google、Bedrock、本地端点等十几家供应商。这章讲
@cline/llms怎么把它们抹平成一个统一的stream(),以及网关在交给内核循环前做的两件公共事:上下文窗口裁剪 和 reasoning 参数归一化。
1. 它要解决的小问题
每家供应商的 API 形状都不同:Anthropic 的 messages 格式、OpenAI 的 chat completions、Bedrock 的封装……如果内核循环(01 章)要直接对接这些,会被各家差异淹没。
所以中间插一层网关:内核只认一个 AgentModel.stream(request) 接口,各家差异由网关内部消化。 内核拿到的永远是统一的事件流(text-delta / tool-call-delta / usage / finish)。
2. 三层抽象
内核循环 网关适配 供应商实现
AgentRuntime ──stream(request)──▶ GatewayModelAdapter ──▶ DefaultGateway.stream ──▶ anthropic.ts / openai.ts / ...
(只认 AgentModel) (把 AgentModelRequest (查注册表、解析模型、 (各家真实 SDK 调用,
翻成 GatewayStreamRequest) 算 maxTokens、调 provider) 吐统一事件)
| 部件 | 职责 | 符号 |
|---|---|---|
AgentModel | 内核眼里的「模型」,只有一个 stream() | @cline/shared 接口 |
GatewayModelAdapter | 把内核的 AgentModelRequest 翻成网关请求 | gateway.ts:53 |
DefaultGateway | 注册表 + 解析 + 上下文裁剪,分发给具体 provider | gateway.ts:212 |
| vendor 实现 | 各家真实 API 调用 | providers/vendors/anthropic.ts 等 |
供应商是注册进来的:DefaultGateway 构造时装内置供应商(BUILTIN_PROVIDER_REGISTRATIONS,gateway.ts:222-234),也接受自定义注册和按 provider 的配置。createGateway(config)(gateway.ts:322)是工厂入口。
3. 关键活儿一:上下文窗口裁剪
模型有上下文上限(context window)。如果输入 prompt 已经很大,还按用户设的 maxTokens 要求输出,可能超窗报错。网关在 resolveGatewayRequestMaxTokens(gateway.ts:168)里把输出上限「夹」到安全值:
// 示意,非源码 —— 提炼自 gateway.ts:168-210 的取最小值逻辑
const caps = [requestedMaxTokens]; // ① 用户要的
if (model.maxOutputTokens) caps.push(model.maxOutputTokens); // ② 模型硬上限
if (model.contextWindow) {
// ③ 上下文里还剩多少:总窗 - 估算的输入 - 给格式化留的余量(1024)
const remaining = model.contextWindow - estimatedInputTokens - RESERVE;
if (remaining <= 0) { onContextOverflow(...); return undefined; } // 装不下,警告
caps.push(remaining);
}
return Math.max(1, Math.min(...caps)); // 取三者最小,至少留 1
重点看那个 remaining 计算。 它不是简单截断,而是:
- 估算输入 token 有多大(
estimateRequestInputTokens,gateway.ts:146—— 把 systemPrompt + messages + tools 序列化后估)。 - 减掉一个固定余量
GATEWAY_OUTPUT_RESERVE_TOKENS = 1024(gateway.ts:22),给供应商的格式化开销、工具 schema 开销、tokenizer 误差留空间。注释说这是故意略微高估输入,宁可保守。 - 三个上限取最小:用户想要的、模型能给的、上下文还剩的。
如果连输入都装不下(remaining <= 0),触发 onContextOverflow 回调打一条 warn 日志(gateway.ts:288),而不是硬塞过去等供应商报错。