Skip to content

构建 Provider 插件

本指南将带你完整地构建一个 Provider 插件,为 OpenClaw 接入一个模型 Provider(LLM)。读完后,你将拥有一个包含模型目录、API Key 认证和动态模型解析的完整 Provider——让你的"小龙虾"能接上更多 AI 大脑。

信息: 如果你还没有构建过任何 OpenClaw 插件,请先阅读 快速入门,了解基础包结构和 manifest 设置。

开发步骤

第一步:创建包和 manifest

json
// package.json
{
  "name": "@myorg/openclaw-acme-ai",
  "version": "1.0.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "providers": ["acme-ai"]
  }
}
json
// openclaw.plugin.json
{
  "id": "acme-ai",
  "name": "Acme AI",
  "description": "Acme AI model provider",
  "providers": ["acme-ai"],
  "providerAuthEnvVars": {
    "acme-ai": ["ACME_AI_API_KEY"]
  },
  "providerAuthChoices": [
    {
      "provider": "acme-ai",
      "method": "api-key",
      "choiceId": "acme-ai-api-key",
      "choiceLabel": "Acme AI API key",
      "groupId": "acme-ai",
      "groupLabel": "Acme AI",
      "cliFlag": "--acme-ai-api-key",
      "cliOption": "--acme-ai-api-key <key>",
      "cliDescription": "Acme AI API key"
    }
  ],
  "configSchema": {
    "type": "object",
    "additionalProperties": false
  }
}

manifest 中声明 providerAuthEnvVars,OpenClaw 就可以在不加载插件运行时的情况下检测凭据。

第二步:注册 Provider

一个最简 Provider 需要 idlabelauthcatalog

typescript
// index.ts
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";

export default definePluginEntry({
  id: "acme-ai",
  name: "Acme AI",
  description: "Acme AI model provider",
  register(api) {
    api.registerProvider({
      id: "acme-ai",
      label: "Acme AI",
      docsPath: "/providers/acme-ai",
      envVars: ["ACME_AI_API_KEY"],

      auth: [
        createProviderApiKeyAuthMethod({
          providerId: "acme-ai",
          methodId: "api-key",
          label: "Acme AI API key",
          hint: "来自 Acme AI 控制台的 API key",
          optionKey: "acmeAiApiKey",
          flagName: "--acme-ai-api-key",
          envVar: "ACME_AI_API_KEY",
          promptMessage: "请输入你的 Acme AI API key",
          defaultModel: "acme-ai/acme-large",
        }),
      ],

      catalog: {
        order: "simple",
        run: async (ctx) => {
          const apiKey =
            ctx.resolveProviderApiKey("acme-ai").apiKey;
          if (!apiKey) return null;
          return {
            provider: {
              baseUrl: "https://api.acme-ai.com/v1",
              apiKey,
              api: "openai-completions",
              models: [
                {
                  id: "acme-large",
                  name: "Acme Large",
                  reasoning: true,
                  input: ["text", "image"],
                  cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
                  contextWindow: 200000,
                  maxTokens: 32768,
                },
                {
                  id: "acme-small",
                  name: "Acme Small",
                  reasoning: false,
                  input: ["text"],
                  cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
                  contextWindow: 128000,
                  maxTokens: 8192,
                },
              ],
            },
          };
        },
      },
    });
  },
});

这是一个可用的 Provider。用户现在可以 openclaw onboard --acme-ai-api-key <key> 并选择 acme-ai/acme-large 作为模型。

对于只注册一个文本 Provider(API key 认证 + 单一目录驱动运行时)的捆绑 Provider,优先使用更窄的 defineSingleProviderPluginEntry(...) 辅助函数:

typescript
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";

export default defineSingleProviderPluginEntry({
  id: "acme-ai",
  name: "Acme AI",
  description: "Acme AI model provider",
  provider: {
    label: "Acme AI",
    docsPath: "/providers/acme-ai",
    auth: [
      {
        methodId: "api-key",
        label: "Acme AI API key",
        hint: "来自 Acme AI 控制台的 API key",
        optionKey: "acmeAiApiKey",
        flagName: "--acme-ai-api-key",
        envVar: "ACME_AI_API_KEY",
        promptMessage: "请输入你的 Acme AI API key",
        defaultModel: "acme-ai/acme-large",
      },
    ],
    catalog: {
      buildProvider: () => ({
        api: "openai-completions",
        baseUrl: "https://api.acme-ai.com/v1",
        models: [{ id: "acme-large", name: "Acme Large" }],
      }),
    },
  },
});

如果你的认证流程还需要在引导期间修补 models.providers.*、别名和 agent 默认模型,使用 openclaw/plugin-sdk/provider-onboard 中的预设辅助工具。最窄的辅助工具是 createDefaultModelPresetAppliers(...)createDefaultModelsPresetAppliers(...)createModelCatalogPresetAppliers(...)

第三步:添加动态模型解析

如果你的 Provider 接受任意模型 ID(如代理或路由器),添加 resolveDynamicModel

typescript
api.registerProvider({
  // ... 上面的 id、label、auth、catalog

  resolveDynamicModel: (ctx) => ({
    id: ctx.modelId,
    name: ctx.modelId,
    provider: "acme-ai",
    api: "openai-completions",
    baseUrl: "https://api.acme-ai.com/v1",
    reasoning: false,
    input: ["text"],
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    contextWindow: 128000,
    maxTokens: 8192,
  }),
});

如果解析需要网络调用,使用 prepareDynamicModel 进行异步预热——完成后 resolveDynamicModel 会再次运行。

第四步:按需添加运行时 hooks

大多数 Provider 只需要 catalog + resolveDynamicModel。按需增量添加 hooks。

Token 交换: 对于每次推理调用前需要 token 交换的 Provider:

typescript
prepareRuntimeAuth: async (ctx) => {
  const exchanged = await exchangeToken(ctx.apiKey);
  return {
    apiKey: exchanged.token,
    baseUrl: exchanged.baseUrl,
    expiresAt: exchanged.expiresAt,
  };
},

自定义请求头: 对于需要自定义请求头或请求体修改的 Provider:

typescript
// wrapStreamFn 返回从 ctx.streamFn 派生的 StreamFn
wrapStreamFn: (ctx) => {
  if (!ctx.streamFn) return undefined;
  const inner = ctx.streamFn;
  return async (params) => {
    params.headers = {
      ...params.headers,
      "X-Acme-Version": "2",
    };
    return inner(params);
  };
},

用量与计费: 对于暴露用量/计费数据的 Provider:

typescript
resolveUsageAuth: async (ctx) => {
  const auth = await ctx.resolveOAuthToken();
  return auth ? { token: auth.token } : null;
},
fetchUsageSnapshot: async (ctx) => {
  return await fetchAcmeUsage(ctx.token, ctx.timeoutMs);
},

所有可用的 Provider hooks:

OpenClaw 按以下顺序调用 hooks。大多数 Provider 只用其中 2-3 个:

#Hook使用场景
1catalog模型目录或 base URL 默认值
2resolveDynamicModel接受任意上游模型 ID
3prepareDynamicModel解析前的异步元数据获取
4normalizeResolvedModel传给 runner 前的传输重写
5capabilities转录/工具元数据(数据,不可调用)
6prepareExtraParams默认请求参数
7wrapStreamFn自定义请求头/请求体包装
8formatApiKey自定义运行时 token 格式
9refreshOAuth自定义 OAuth 刷新
10buildAuthDoctorHint认证修复指引
11isCacheTtlEligiblePrompt 缓存 TTL 门控
12buildMissingAuthMessage自定义缺失认证提示
13suppressBuiltInModel隐藏过时的上游行
14augmentModelCatalog合成前向兼容行
15isBinaryThinking二值思考开关
16supportsXHighThinkingxhigh 推理支持
17resolveDefaultThinkingLevel默认 /think 策略
18isModernModelRef实时/冒烟模型匹配
19prepareRuntimeAuth推理前的 token 交换
20resolveUsageAuth自定义用量凭据解析
21fetchUsageSnapshot自定义用量端点
22onModelSelected选择后回调(如遥测)

详细描述和真实示例见内部架构:Provider 运行时 hooks

第五步:添加附加能力(可选)

Provider 插件可以在文本推理之外注册语音、媒体理解、图像生成和网络搜索:

typescript
register(api) {
  api.registerProvider({ id: "acme-ai", /* ... */ });

  api.registerSpeechProvider({
    id: "acme-ai",
    label: "Acme Speech",
    isConfigured: ({ config }) => Boolean(config.messages?.tts),
    synthesize: async (req) => ({
      audioBuffer: Buffer.from(/* PCM 数据 */),
      outputFormat: "mp3",
      fileExtension: ".mp3",
      voiceCompatible: false,
    }),
  });

  api.registerMediaUnderstandingProvider({
    id: "acme-ai",
    capabilities: ["image", "audio"],
    describeImage: async (req) => ({ text: "一张照片..." }),
    transcribeAudio: async (req) => ({ text: "转录文字..." }),
  });

  api.registerImageGenerationProvider({
    id: "acme-ai",
    label: "Acme Images",
    generate: async (req) => ({ /* 图像结果 */ }),
  });
}

OpenClaw 会将此分类为 hybrid-capability 插件。这是公司插件(每个厂商一个插件)的推荐模式。见内部架构:能力归属

第六步:编写测试

typescript
// src/provider.test.ts
import { describe, it, expect } from "vitest";
// 从 index.ts 或专用文件导出你的 provider 配置对象
import { acmeProvider } from "./provider.js";

describe("acme-ai provider", () => {
  it("解析动态模型", () => {
    const model = acmeProvider.resolveDynamicModel!({
      modelId: "acme-beta-v3",
    } as any);
    expect(model.id).toBe("acme-beta-v3");
    expect(model.provider).toBe("acme-ai");
  });

  it("有 key 时返回目录", async () => {
    const result = await acmeProvider.catalog!.run({
      resolveProviderApiKey: () => ({ apiKey: "test-key" }),
    } as any);
    expect(result?.provider?.models).toHaveLength(2);
  });

  it("无 key 时返回 null 目录", async () => {
    const result = await acmeProvider.catalog!.run({
      resolveProviderApiKey: () => ({ apiKey: undefined }),
    } as any);
    expect(result).toBeNull();
  });
});

文件结构

extensions/acme-ai/
├── package.json              # openclaw.providers 元数据
├── openclaw.plugin.json      # 含 providerAuthEnvVars 的 manifest
├── index.ts                  # definePluginEntry + registerProvider
└── src/
    ├── provider.test.ts      # 测试
    └── usage.ts              # 用量端点(可选)

catalog.order 参考

catalog.order 控制你的目录相对于内置 Provider 的合并时机:

Order时机使用场景
simple第一轮普通 API key Provider
profilesimple 之后依赖认证配置文件的 Provider
pairedprofile 之后合成多个相关条目
late最后一轮覆盖已有 Provider(碰撞时胜出)

下一步