LLM Providers:厂商适配
职责
LLM provider 是把不同厂商 API 适配成统一流式接口的薄层。Anthropic、OpenAI Responses、OpenAI Completions、Azure OpenAI、Google Gemini、Google Vertex、Mistral、OpenAI Codex(ChatGPT Responses)——每家 API 的请求/响应/流式事件格式都不一样,但 OpenClaw 的 agent 主循环只想要一种事件流:AssistantMessageEventStream(thinking / text / toolCall / toolResult / error / stop)。provider 的工作就是在中间做翻译:把模型 API 的 SSE/JSON 流解析成 OpenClaw 的统一事件流。
provider 不做"思考",只做四件事:建客户端(API key、OAuth、headers、cache策略)、构造请求参数(messages/tool definitions/options 折成厂商格式)、解析流式响应(把厂商的事件序列翻译成统一事件)、处理错误(把 HTTP 错误、流截断、message_stop 缺失等都编码成 stopReason: "error")。所有 provider 实现同一个 StreamFunction 契约(types.ts StreamFunction:L201-L208),因此主循环不用关心在调哪家。
设计动机
为什么把 provider 抽象成 StreamFunction?因为厂商差异大但调用语义同构。每个 provider 都接受 (model, context, options),都返回 AssistantMessageEventStream——这种"输入/输出同构、内部实现各异"是接口抽象的理想场景。主循环只调 stream(model, context, options),具体走哪家由 model.api 字段决定。
Api 类型(types.ts KnownApi:L6-L18) 列了所有内置 API id:
export type KnownApi =
| "openai-completions"
| "mistral-conversations"
| "openai-responses"
| "azure-openai-responses"
| "openai-chatgpt-responses"
| "anthropic-messages"
| "bedrock-converse-stream"
| "google-generative-ai"
| "google-vertex";
export type Api = KnownApi | (string & {});注意末尾的 | (string & {})——这是 TypeScript 里"允许任意字符串但保留自动补全"的技巧。它意味着 Api 不被锁死在 KnownApi 集合里,第三方插件可以注册自己的 provider 走自定义 api id(比如 "my-custom-llm")。registerApiProvider(api-registry.ts registerApiProvider:L77-L90) 接受任意 TApi extends Api,然后存到一张 Map<string, RegisteredApiProvider> 里。
StreamFunction 契约(packages/llm-core/src/types.ts:L193-L208) 写得很清楚:
// Contract:
// - Must return an AssistantMessageEventStream.
// - Once invoked, request/model/runtime failures should be encoded in the
// returned stream, not thrown.
// - Error termination must produce an AssistantMessage with stopReason
// "error" or "aborted" and errorMessage, emitted via the stream protocol.
export type StreamFunction<
TApi extends Api = Api,
TOptions extends StreamOptions = StreamOptions,
> = (
model: Model<TApi>,
context: Context,
options?: TOptions,
) => AssistantMessageEventStreamContract;关键契约:错误不抛——一旦 provider 被调用,后续的失败(网络错、模型拒绝、流截断)必须编码进返回的 stream 里,以一个 stopReason: "error" 的 AssistantMessage 结束。这个契约让主循环的 try/catch 可以专注于"调 provider 之前"的错(参数错、api 未注册),provider 内部错全部走 stream 处理路径——统一了错误处理逻辑。
懒加载是另一个重要设计(register-builtins.ts 懒加载:L156-L181):
function createLazyStream<
TApi extends Api,
TOptions extends StreamOptions,
TSimpleOptions extends SimpleStreamOptions,
>(
loadModule: () => Promise<LazyProviderModule<TApi, TOptions, TSimpleOptions>>,
): StreamFunction<TApi, TOptions> {
return (model, context, options) => {
const outer = new AssistantMessageEventStream();
loadModule()
.then((module) => {
const inner = module.stream(model, context, options);
forwardStream(outer, inner);
})
.catch((error: unknown) => {
const message = createLazyLoadErrorMessage(model, error);
outer.push({ type: "error", reason: "error", error: message });
outer.end(message);
});
return outer;
};
}provider 模块只在第一次实际被调用时才 import——这避免了"启动时强制加载所有 provider"的冷启动成本。如果用户只调 Anthropic,OpenAI/Google/Mistral 的代码根本不会被加载。加载失败(模块缺失、语法错)也被编码成 stream 里的 error 事件,不抛错——遵守同一个契约。
registerBuiltInApiProviders(register-builtins.ts 注册所有内置:L342-L414) 把所有内置 provider 注册进 apiProviderRegistry,每条都带 sourceId: BUILT_IN_API_PROVIDER_SOURCE_ID,方便 unregisterApiProviders(sourceId) 在测试 teardown 时一键清空。
custom provider 也能注册:custom-api-registry.ts ensureCustomApiRegistered:L15-L36 给 SDK 调用方一个口子,但加了"已有 provider 就不注册"的守卫——避免覆盖内置实现。
关键文件
types.ts KnownApi:L6-L18— 内置 9 种 API id。types.ts StreamFunction:L193-L208— provider 契约:错误进 stream 不抛。api-registry.ts 类型与注册表:L13-L50—ApiProvider接口与内部 Map。api-registry.ts registerApiProvider:L77-L90— 注册/替换 API provider。api-registry.ts getApiProvider:L93-L100— 按 api id 查询。stream.ts stream/complete:L22-L40— 主入口stream与complete,按model.api分发。register-builtins.ts 懒加载:L156-L181—createLazyStream包装。register-builtins.ts 导出懒 stream:L316-L339— 9 个内置 provider 的懒 stream 导出。register-builtins.ts registerBuiltInApiProviders:L342-L414— 注册到 registry。anthropic.ts streamAnthropic:L447-L535— Anthropic provider 实现。openai-responses.ts streamOpenAIResponses:L76-L106— OpenAI Responses provider。custom-api-registry.ts:L15-L36— SDK 自定义 provider 注册口。openclaw-provider-index.ts:L13-L69— 预声明 provider 索引,未装插件时模型选择器也能看到。
数据流
主循环调 stream(stream.ts stream:L22-L30):
function resolveApiProvider(api: Api) {
const provider = getApiProvider(api);
if (!provider) {
throw new Error(`No API provider registered for api: ${api}`);
}
return provider;
}
export function stream<TApi extends Api>(
model: Model<TApi>,
context: Context,
options?: ProviderStreamOptions,
): AssistantMessageEventStreamContract {
const provider = resolveApiProvider(model.api);
return provider.stream(model, context, options as StreamOptions);
}注意"api 未注册"在这里是抛错的——发生在调 provider 之前,主循环可以 catch。一旦进入 provider.stream,后续错误全部走 stream。
注册表内部是一个简单 Map(packages/llm-runtime/src/api-registry.ts:L45-L90):
const apiProviderRegistry = new Map<string, RegisteredApiProvider>();
export function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(
provider: ApiProvider<TApi, TOptions>,
sourceId?: string,
): void {
apiProviderRegistry.set(provider.api, {
provider: {
api: provider.api,
stream: wrapStream(provider.api, provider.stream),
streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
},
sourceId,
});
}wrapStream 会校验 model.api 是否匹配,不匹配抛 Mismatched api——这是为类型降级设的防线:外部代码传 Model<"openai-responses"> 给 stream 函数,运行时 model.api 必须真的等于 "openai-responses",否则就是类型+运行时不一致。
Anthropic provider 的实现(anthropic.ts streamAnthropic:L447-L535) 是典型样本:
export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions> = (
model: Model<"anthropic-messages">,
context: Context,
options?: AnthropicOptions,
) => {
const stream = new AssistantMessageEventStream();
void (async () => {
const output: AssistantMessage = {
role: "assistant",
content: [],
api: model.api as Api,
provider: model.provider,
model: model.id,
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
stopReason: "stop",
timestamp: Date.now(),
};
// Fable classifiers can refuse after partial generation, so no event is
// safe to expose until the terminal stop reason is known.
const refusalBuffer = usesClaudeFable5MessagesContract(model)
? createDeferredEventBuffer<AssistantMessageEvent>(stream, () =>
notifyLlmRequestActivity(options?.signal),
)
: undefined;
const eventSink = refusalBuffer ?? stream;
try {
let client: Anthropic;
// ... 建 client、buildParams、client.messages.create stream:true、解析事件注意 Fable 系列(Claude 5 系列里某些 classifier 模型)有个特殊行为:可能在生成中途拒绝。所以 provider 不能"边收边吐"——必须缓冲到 stopReason 已知才暴露事件。createDeferredEventBuffer 就是这种缓冲器,其他模型直接走 stream。这是 provider 适配厂商特定行为的一个例子:Anthropic 这家有一个特殊角落,就内嵌在 provider 里,不污染主循环。
OpenAI Responses provider 同构(src/llm/providers/openai-responses.ts:L76-L106),只是少了 Fable 缓冲逻辑——因为这是 Anthropic 特有的。
边界与失败
- 错误进 stream 不抛:
StreamFunction契约(packages/llm-core/src/types.ts:L196-L200) 明确规定"request/model/runtime failures should be encoded in the returned stream, not thrown"。provider 内部的 HTTP 错、流截断、message_stop 缺失都得编成stopReason: "error"的AssistantMessage。anthropic.ts在src/llm/providers/anthropic.ts:L442-L444显式检测if (sawMessageStart && !sawMessageEnd) throw,但这个 throw 在 async IIFE 里被 catch 后转成 stream error 事件。 - 懒加载失败编码进 stream:
createLazyStream的.catch把模块加载失败转成outer.push({ type: "error" })(src/llm/providers/register-builtins.ts:L172-L177)。这让"provider SDK 没装"和"API 调用失败"走同一条处理路径,主循环无需区分。 - 类型/运行时不一致抛错:
wrapStream检查model.api !== api时抛Mismatched api(packages/llm-runtime/src/api-registry.ts:L52-L62)。这是为类型降级设的防线,发生在 stream 真正被调用之前。 - api 未注册抛错:
resolveApiProvider找不到 provider 抛No API provider registered for api: ...(packages/llm-runtime/src/stream.ts:L14-L20)。这是配置错误,不在 stream 契约覆盖范围内,主循环 catch 后该报错给用户。 - Fable 缓冲:
usesClaudeFable5MessagesContract(model)决定是否启用createDeferredEventBuffer(src/llm/providers/anthropic.ts:L474-L479)。这是 Anthropic 特定的——其他 provider 没有这种"中途拒绝"行为,直接走 stream。 - Cache retention 策略:
resolveCacheRetention()决定是否带 cache session id(src/llm/providers/anthropic.ts:L500-L511)。"none"时显式不传cacheSessionId,避免误启用 prompt cache。 - GitHub Copilot 动态 headers:provider 内部还处理了
github-copilotprovider 的特殊 headers(buildCopilotDynamicHeaders),这是把 GitHub Copilot 的 Claude 当 Anthropic provider 用时的适配(src/llm/providers/anthropic.ts:L492-L498)——一个 provider 内部适配多个 provider id。 - 预声明索引:
OPENCLAW_PROVIDER_INDEX(src/model-catalog/provider-index/openclaw-provider-index.ts:L13-L69) 让模型选择器在插件未装时也能看到 Moonshot/DeepSeek 等 provider 的预声明信息。插件真装上后,plugin manifest 才是权威。 - custom provider 不覆盖内置:
ensureCustomApiRegistered先getApiProvider(api)检查,已有就返回 false(src/agents/custom-api-registry.ts:L17-L19)。这防止 SDK 注册的 custom provider 误覆盖内置实现。
小结
provider 是适配不同厂商 API 的薄层:9 个内置 provider 覆盖 Anthropic/OpenAI/Google/Mistral/Azure/Vertex/Codex,通过 registerApiProvider 注册进 apiProviderRegistry Map。StreamFunction 契约规定错误进 stream 不抛,懒加载让冷启动只加载用到的 provider。Anthropic Fable 缓冲、Copilot 动态 headers 这些厂商特定角落被封装在对应 provider 内部,不污染主循环。主循环通过 Agent 主循环 调 stream(model, context, options),具体走哪家由 model.api 决定——配置层只需要在 openclaw.json 里声明 provider 的 apiKey/baseUrl/models。
对照官方资料:Providers 文档 · README。