Building Provider Plugins
Building Provider Plugins
Section titled “Building Provider Plugins”This guide walks through building a provider plugin that adds a model provider (LLM) to OpenClaw. By the end you will have a provider with a model catalog, API key auth, and dynamic model resolution.
Walkthrough
Section titled “Walkthrough”Package and manifest
{"name": "@myorg/openclaw-acme-ai","version": "1.0.0","type": "module","openclaw": {"extensions": ["./index.ts"],"providers": ["acme-ai"],"compat": {"pluginApi": ">=2026.3.24-beta.2","minGatewayVersion": "2026.3.24-beta.2"},"build": {"openclawVersion": "2026.3.24-beta.2","pluginSdkVersion": "2026.3.24-beta.2"}}}{"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”, “cliDescription”: “Acme AI API key” } ], “configSchema”: { “type”: “object”, “additionalProperties”: false } } ```
The manifest declares
providerAuthEnvVarsso OpenClaw can detect credentials without loading your plugin runtime. If you publish the provider on ClawHub, thoseopenclaw.compatandopenclaw.buildfields are required inpackage.json.Register the provider
A minimal provider needs an
id,label,auth, andcatalog: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: "API key from your Acme AI dashboard",optionKey: "acmeAiApiKey",flagName: "--acme-ai-api-key",envVar: "ACME_AI_API_KEY",promptMessage: "Enter your 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,},],},};},},});},});That is a working provider. Users can now `openclaw onboard —acme-ai-api-key
and selectacme-ai/acme-large` as their model.For bundled providers that only register one text provider with API-keyauth plus a single catalog-backed runtime, prefer the narrower`defineSingleProviderPluginEntry(...)` helper:```typescriptimport { 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: "API key from your Acme AI dashboard",optionKey: "acmeAiApiKey",flagName: "--acme-ai-api-key",envVar: "ACME_AI_API_KEY",promptMessage: "Enter your 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" }],}),},},});```If your auth flow also needs to patch `models.providers.*`, aliases, andthe agent default model during onboarding, use the preset helpers from`openclaw/plugin-sdk/provider-onboard`. The narrowest helpers are`createDefaultModelPresetAppliers(...)`,`createDefaultModelsPresetAppliers(...)`, and`createModelCatalogPresetAppliers(...)`.Add dynamic model resolution
If your provider accepts arbitrary model IDs (like a proxy or router), add
resolveDynamicModel:api.registerProvider({// ... id, label, auth, catalog from aboveresolveDynamicModel: (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,}),});If resolving requires a network call, use
prepareDynamicModelfor async warm-up —resolveDynamicModelruns again after it completes.Add runtime hooks (as needed)
Most providers only need
catalog+resolveDynamicModel. Add hooks incrementally as your provider requires them.For providers that need a token exchange before each inference call:
prepareRuntimeAuth: async (ctx) => {const exchanged = await exchangeToken(ctx.apiKey);return {apiKey: exchanged.token,baseUrl: exchanged.baseUrl,expiresAt: exchanged.expiresAt,};},For providers that need custom request headers or body modifications:
// wrapStreamFn returns a StreamFn derived from ctx.streamFnwrapStreamFn: (ctx) => {if (!ctx.streamFn) return undefined;const inner = ctx.streamFn;return async (params) => {params.headers = {...params.headers,"X-Acme-Version": "2",};return inner(params);};},For providers that expose usage/billing data:
resolveUsageAuth: async (ctx) => {const auth = await ctx.resolveOAuthToken();return auth ? { token: auth.token } : null;},fetchUsageSnapshot: async (ctx) => {return await fetchAcmeUsage(ctx.token, ctx.timeoutMs);},All available provider hooks
OpenClaw calls hooks in this order. Most providers only use 2-3:
# Hook When to use 1 catalogModel catalog or base URL defaults 2 resolveDynamicModelAccept arbitrary upstream model IDs 3 prepareDynamicModelAsync metadata fetch before resolving 4 normalizeResolvedModelTransport rewrites before the runner 5 capabilitiesTranscript/tooling metadata (data, not callable) 6 prepareExtraParamsDefault request params 7 wrapStreamFnCustom headers/body wrappers 8 formatApiKeyCustom runtime token shape 9 refreshOAuthCustom OAuth refresh 10 buildAuthDoctorHintAuth repair guidance 11 isCacheTtlEligiblePrompt cache TTL gating 12 buildMissingAuthMessageCustom missing-auth hint 13 suppressBuiltInModelHide stale upstream rows 14 augmentModelCatalogSynthetic forward-compat rows 15 isBinaryThinkingBinary thinking on/off 16 supportsXHighThinkingxhighreasoning support17 resolveDefaultThinkingLevelDefault /thinkpolicy18 isModernModelRefLive/smoke model matching 19 prepareRuntimeAuthToken exchange before inference 20 resolveUsageAuthCustom usage credential parsing 21 fetchUsageSnapshotCustom usage endpoint 22 onModelSelectedPost-selection callback (e.g. telemetry) 23 buildReplayPolicyCustom transcript policy (e.g. thinking-block stripping) 24 sanitizeReplayHistoryProvider-specific replay rewrites after generic cleanup 25 validateReplayTurnsStrict replay-turn validation before the embedded runner For detailed descriptions and real-world examples, see Internals: Provider Runtime Hooks.
Add extra capabilities (optional)
A provider plugin can register speech, media understanding, image generation, and web search alongside text inference:
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 data */),outputFormat: "mp3",fileExtension: ".mp3",voiceCompatible: false,}),});api.registerMediaUnderstandingProvider({id: "acme-ai",capabilities: ["image", "audio"],describeImage: async (req) => ({ text: "A photo of..." }),transcribeAudio: async (req) => ({ text: "Transcript..." }),});api.registerImageGenerationProvider({id: "acme-ai",label: "Acme Images",generate: async (req) => ({ /* image result */ }),});}OpenClaw classifies this as a hybrid-capability plugin. This is the recommended pattern for company plugins (one plugin per vendor). See Internals: Capability Ownership.
Test
import { describe, it, expect } from "vitest";// Export your provider config object from index.ts or a dedicated fileimport { acmeProvider } from "./provider.js";describe("acme-ai provider", () => {it("resolves dynamic models", () => {const model = acmeProvider.resolveDynamicModel!({modelId: "acme-beta-v3",} as any);expect(model.id).toBe("acme-beta-v3");expect(model.provider).toBe("acme-ai");});it("returns catalog when key is available", async () => {const result = await acmeProvider.catalog!.run({resolveProviderApiKey: () => ({ apiKey: "test-key" }),} as any);expect(result?.provider?.models).toHaveLength(2);});it("returns null catalog when no key", async () => {const result = await acmeProvider.catalog!.run({resolveProviderApiKey: () => ({ apiKey: undefined }),} as any);expect(result).toBeNull();});});
Publish to ClawHub
Section titled “Publish to ClawHub”Provider plugins publish the same way as any other external code plugin:
clawhub package publish your-org/your-plugin --dry-runclawhub package publish your-org/your-pluginDo not use the legacy skill-only publish alias here; plugin packages should use
clawhub package publish.
File structure
Section titled “File structure”/acme-ai/ ├── package.json # openclaw.providers metadata ├── openclaw.plugin.json # Manifest with providerAuthEnvVars ├── index.ts # definePluginEntry + registerProvider └── src/ ├── provider.test.ts # Tests └── usage.ts # Usage endpoint (optional)
## Catalog order reference
`catalog.order` controls when your catalog merges relative to built-inproviders:
| Order | When | Use case || --------- | ------------- | ----------------------------------------------- || `simple` | First pass | Plain API-key providers || `profile` | After simple | Providers gated on auth profiles || `paired` | After profile | Synthesize multiple related entries || `late` | Last pass | Override existing providers (wins on collision) |
## Next steps
- [Channel Plugins](/en/plugins/sdk-channel-plugins) — if your plugin also provides a channel- [SDK Runtime](/en/plugins/sdk-runtime) — `api.runtime` helpers (TTS, search, subagent)- [SDK Overview](/en/plugins/sdk-overview) — full subpath import reference- [Plugin Internals](/en/plugins/architecture#provider-runtime-hooks) — hook details and bundled examples