Skip to content

LLM Providers:厂商适配

源码版本v2026.6.11

职责

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:

typescript
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) 写得很清楚:

typescript
// 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):

typescript
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 就不注册"的守卫——避免覆盖内置实现。

关键文件

数据流

主循环调 stream(stream.ts stream:L22-L30):

typescript
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):

typescript
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) 是典型样本:

typescript
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"AssistantMessageanthropic.tssrc/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-copilot provider 的特殊 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 不覆盖内置:ensureCustomApiRegisteredgetApiProvider(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