Appearance
构建 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 需要 id、label、auth 和 catalog:
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 | 使用场景 |
|---|---|---|
| 1 | catalog | 模型目录或 base URL 默认值 |
| 2 | resolveDynamicModel | 接受任意上游模型 ID |
| 3 | prepareDynamicModel | 解析前的异步元数据获取 |
| 4 | normalizeResolvedModel | 传给 runner 前的传输重写 |
| 5 | capabilities | 转录/工具元数据(数据,不可调用) |
| 6 | prepareExtraParams | 默认请求参数 |
| 7 | wrapStreamFn | 自定义请求头/请求体包装 |
| 8 | formatApiKey | 自定义运行时 token 格式 |
| 9 | refreshOAuth | 自定义 OAuth 刷新 |
| 10 | buildAuthDoctorHint | 认证修复指引 |
| 11 | isCacheTtlEligible | Prompt 缓存 TTL 门控 |
| 12 | buildMissingAuthMessage | 自定义缺失认证提示 |
| 13 | suppressBuiltInModel | 隐藏过时的上游行 |
| 14 | augmentModelCatalog | 合成前向兼容行 |
| 15 | isBinaryThinking | 二值思考开关 |
| 16 | supportsXHighThinking | xhigh 推理支持 |
| 17 | resolveDefaultThinkingLevel | 默认 /think 策略 |
| 18 | isModernModelRef | 实时/冒烟模型匹配 |
| 19 | prepareRuntimeAuth | 推理前的 token 交换 |
| 20 | resolveUsageAuth | 自定义用量凭据解析 |
| 21 | fetchUsageSnapshot | 自定义用量端点 |
| 22 | onModelSelected | 选择后回调(如遥测) |
详细描述和真实示例见内部架构: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 |
profile | simple 之后 | 依赖认证配置文件的 Provider |
paired | profile 之后 | 合成多个相关条目 |
late | 最后一轮 | 覆盖已有 Provider(碰撞时胜出) |
下一步
- 渠道插件 — 如果你的插件同时提供渠道
- SDK 运行时辅助工具 —
api.runtime辅助工具(TTS、搜索、subagent) - SDK 概览 — 完整子路径导入参考
- 内部架构:Provider 运行时 hooks