2026-03-22 08:50:43 -07:00
---
2026-03-22 11:35:53 -07:00
title: "Building Provider Plugins"
2026-03-22 08:50:43 -07:00
sidebarTitle: "Provider Plugins"
2026-03-22 11:35:53 -07:00
summary: "Step-by-step guide to building a model provider plugin for OpenClaw"
2026-03-22 08:50:43 -07:00
read_when:
2026-03-22 11:35:53 -07:00
- You are building a new model provider plugin
- You want to add an OpenAI-compatible proxy or custom LLM to OpenClaw
- You need to understand provider auth, catalogs, and runtime hooks
2026-03-22 08:50:43 -07:00
---
2026-03-22 11:35:53 -07:00
# 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.
<Info>
If you have not built any OpenClaw plugin before, read
[Getting Started ](/plugins/building-plugins ) first for the basic package
structure and manifest setup.
</Info>
## Walkthrough
<Steps>
2026-03-30 01:21:00 +02:00
<a id="step-1-package-and-manifest"></a>
2026-03-22 11:35:53 -07:00
<Step title="Package and manifest">
<CodeGroup>
```json package.json
{
"name": "@myorg/openclaw-acme-ai",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
2026-03-29 11:59:19 -07:00
"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"
}
2026-03-22 11:35:53 -07:00
}
}
` ``
` ``json openclaw.plugin.json
{
"id": "acme-ai",
"name": "Acme AI",
"description": "Acme AI model provider",
"providers": ["acme-ai"],
2026-04-04 04:52:14 +01:00
"modelSupport": {
"modelPrefixes": ["acme-"]
},
2026-03-22 11:35:53 -07:00
"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
}
}
` ``
</CodeGroup>
The manifest declares ` providerAuthEnvVars` so OpenClaw can detect
2026-04-04 04:52:14 +01:00
credentials without loading your plugin runtime. ` modelSupport` is optional
and lets OpenClaw auto-load your provider plugin from shorthand model ids
like ` acme-large` before runtime hooks exist. If you publish the
2026-03-29 11:59:19 -07:00
provider on ClawHub, those ` openclaw.compat` and ` openclaw.build` fields
are required in ` package.json`.
2026-03-22 11:35:53 -07:00
</Step>
<Step title="Register the provider">
A minimal provider needs an ` id`, ` label`, ` auth`, and ` 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: "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",
}),
],
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
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,
},
],
},
};
},
},
});
},
2026-03-22 08:50:43 -07:00
});
2026-03-22 11:35:53 -07:00
` ``
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
That is a working provider. Users can now
` openclaw onboard --acme-ai-api-key <key>` and select
` acme-ai/acme-large` as their model.
2026-03-22 08:50:43 -07:00
2026-03-23 01:34:24 +00:00
For bundled providers that only register one text provider with API-key
auth plus a single catalog-backed runtime, prefer the narrower
` defineSingleProviderPluginEntry(...)` helper:
` ``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: "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" }],
}),
},
},
});
` ``
2026-03-23 01:39:10 +00:00
If your auth flow also needs to patch ` models.providers.*`, aliases, and
the agent default model during onboarding, use the preset helpers from
` openclaw/plugin-sdk/provider-onboard`. The narrowest helpers are
` createDefaultModelPresetAppliers(...)`,
` createDefaultModelsPresetAppliers(...)`, and
` createModelCatalogPresetAppliers(...)`.
2026-04-04 12:20:19 +01:00
When a provider's native endpoint supports streamed usage blocks on the
normal ` openai-completions` transport, prefer the shared catalog helpers in
` openclaw/plugin-sdk/provider-catalog-shared` instead of hardcoding
provider-id checks. ` supportsNativeStreamingUsageCompat(...)` and
` applyProviderNativeStreamingUsageCompat(...)` detect support from the
endpoint capability map, so native Moonshot/DashScope-style endpoints still
opt in even when a plugin is using a custom provider id.
2026-03-22 11:35:53 -07:00
</Step>
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
<Step title="Add dynamic model resolution">
If your provider accepts arbitrary model IDs (like a proxy or router),
add ` resolveDynamicModel`:
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
` ``typescript
api.registerProvider({
// ... id, label, auth, catalog from above
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,
2026-03-22 08:50:43 -07:00
}),
2026-03-22 11:35:53 -07:00
});
` ``
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
If resolving requires a network call, use ` prepareDynamicModel` for async
warm-up — ` resolveDynamicModel` runs again after it completes.
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
</Step>
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
<Step title="Add runtime hooks (as needed)">
Most providers only need ` catalog` + ` resolveDynamicModel`. Add hooks
incrementally as your provider requires them.
2026-03-22 08:50:43 -07:00
2026-04-04 11:38:25 +01:00
Shared helper builders now cover the most common replay/tool-compat
families, so plugins usually do not need to hand-wire each hook one by one:
` ``typescript
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
2026-04-04 12:21:30 +01:00
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream";
2026-04-04 11:38:25 +01:00
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
const GOOGLE_FAMILY_HOOKS = {
...buildProviderReplayFamilyHooks({ family: "google-gemini" }),
2026-04-04 12:21:30 +01:00
...buildProviderStreamFamilyHooks("google-thinking"),
2026-04-04 11:38:25 +01:00
...buildProviderToolCompatFamilyHooks("gemini"),
};
api.registerProvider({
id: "acme-gemini-compatible",
// ...
...GOOGLE_FAMILY_HOOKS,
});
` ``
Available replay families today:
2026-04-04 12:15:19 +01:00
| Family | What it wires in |
| --- | --- |
| ` openai-compatible` | Shared OpenAI-style replay policy for OpenAI-compatible transports, including tool-call-id sanitation, assistant-first ordering fixes, and generic Gemini-turn validation where the transport needs it |
| ` anthropic-by-model` | Claude-aware replay policy chosen by ` modelId`, so Anthropic-message transports only get Claude-specific thinking-block cleanup when the resolved model is actually a Claude id |
| ` google-gemini` | Native Gemini replay policy plus bootstrap replay sanitation and tagged reasoning-output mode |
| ` passthrough-gemini` | Gemini thought-signature sanitation for Gemini models running through OpenAI-compatible proxy transports; does not enable native Gemini replay validation or bootstrap rewrites |
| ` hybrid-anthropic-openai` | Hybrid policy for providers that mix Anthropic-message and OpenAI-compatible model surfaces in one plugin; optional Claude-only thinking-block dropping stays scoped to the Anthropic side |
Real bundled examples:
2026-04-06 12:18:18 +01:00
- ` google` and ` google-gemini-cli`: ` google-gemini`
2026-04-04 12:15:19 +01:00
- ` openrouter`, ` kilocode`, ` opencode`, and ` opencode-go`: ` passthrough-gemini`
2026-04-04 15:53:24 +01:00
- ` amazon-bedrock` and ` anthropic-vertex`: ` anthropic-by-model`
2026-04-04 12:15:19 +01:00
- ` minimax`: ` hybrid-anthropic-openai`
- ` moonshot`, ` ollama`, ` xai`, and ` zai`: ` openai-compatible`
2026-04-04 11:38:25 +01:00
2026-04-04 12:21:30 +01:00
Available stream families today:
| Family | What it wires in |
| --- | --- |
| ` google-thinking` | Gemini thinking payload normalization on the shared stream path |
2026-04-04 16:21:21 +01:00
| ` kilocode-thinking` | Kilo reasoning wrapper on the shared proxy stream path, with ` kilo/auto` and unsupported proxy reasoning ids skipping injected thinking |
2026-04-04 12:21:30 +01:00
| ` moonshot-thinking` | Moonshot binary native-thinking payload mapping from config + ` /think` level |
| ` minimax-fast-mode` | MiniMax fast-mode model rewrite on the shared stream path |
2026-04-04 12:32:30 +01:00
| ` openai-responses-defaults` | Shared native OpenAI/Codex Responses wrappers: attribution headers, ` /fast`/` serviceTier`, text verbosity, native Codex web search, reasoning-compat payload shaping, and Responses context management |
| ` openrouter-thinking` | OpenRouter reasoning wrapper for proxy routes, with unsupported-model/` auto` skips handled centrally |
2026-04-04 12:21:30 +01:00
| ` tool-stream-default-on` | Default-on ` tool_stream` wrapper for providers like Z.AI that want tool streaming unless explicitly disabled |
Real bundled examples:
2026-04-06 12:18:18 +01:00
- ` google` and ` google-gemini-cli`: ` google-thinking`
2026-04-04 16:21:21 +01:00
- ` kilocode`: ` kilocode-thinking`
2026-04-04 12:21:30 +01:00
- ` moonshot`: ` moonshot-thinking`
- ` minimax` and ` minimax-portal`: ` minimax-fast-mode`
2026-04-04 12:32:30 +01:00
- ` openai` and ` openai-codex`: ` openai-responses-defaults`
- ` openrouter`: ` openrouter-thinking`
2026-04-04 12:21:30 +01:00
- ` zai`: ` tool-stream-default-on`
2026-04-04 15:53:24 +01:00
` openclaw/plugin-sdk/provider-model-shared` also exports the replay-family
enum plus the shared helpers those families are built from. Common public
exports include:
- ` ProviderReplayFamily`
- ` buildProviderReplayFamilyHooks(...)`
- shared replay builders such as ` buildOpenAICompatibleReplayPolicy(...)`,
` buildAnthropicReplayPolicyForModel(...)`,
` buildGoogleGeminiReplayPolicy(...)`, and
` buildHybridAnthropicOrOpenAIReplayPolicy(...)`
- Gemini replay helpers such as ` sanitizeGoogleGeminiReplayHistory(...)`
and ` resolveTaggedReasoningOutputMode()`
- endpoint/model helpers such as ` resolveProviderEndpoint(...)`,
` normalizeProviderId(...)`, ` normalizeGooglePreviewModelId(...)`, and
` normalizeNativeXaiModelId(...)`
` openclaw/plugin-sdk/provider-stream` exposes both the family builder and
the public wrapper helpers those families reuse. Common public exports
include:
- ` ProviderStreamFamily`
- ` buildProviderStreamFamilyHooks(...)`
- ` composeProviderStreamWrappers(...)`
- shared OpenAI/Codex wrappers such as
` createOpenAIAttributionHeadersWrapper(...)`,
` createOpenAIFastModeWrapper(...)`,
` createOpenAIServiceTierWrapper(...)`,
` createOpenAIResponsesContextManagementWrapper(...)`, and
` createCodexNativeWebSearchWrapper(...)`
- shared proxy/provider wrappers such as ` createOpenRouterWrapper(...)`,
` createToolStreamWrapper(...)`, and ` createMinimaxFastModeWrapper(...)`
2026-04-04 12:49:53 +01:00
Some stream helpers stay provider-local on purpose. Current bundled
example: ` @openclaw/anthropic -provider` exports
` wrapAnthropicProviderStream`, ` resolveAnthropicBetas`,
` resolveAnthropicFastMode`, ` resolveAnthropicServiceTier`, and the
lower-level Anthropic wrapper builders from its public ` api.ts` /
` contract-api.ts` seam. Those helpers remain Anthropic-specific because
they also encode Claude OAuth beta handling and ` context1m` gating.
2026-04-04 15:34:57 +01:00
Other bundled providers also keep transport-specific wrappers local when
the behavior is not shared cleanly across families. Current example: the
bundled xAI plugin keeps native xAI Responses shaping in its own
` wrapStreamFn`, including ` /fast` alias rewrites, default ` tool_stream`,
unsupported strict-tool cleanup, and xAI-specific reasoning-payload
removal.
2026-04-04 15:38:56 +01:00
` openclaw/plugin-sdk/provider-tools` currently exposes one shared
2026-04-04 15:53:24 +01:00
tool-schema family plus shared schema/compat helpers:
2026-04-04 15:38:56 +01:00
2026-04-04 15:53:24 +01:00
- ` ProviderToolCompatFamily` documents the shared family inventory today.
2026-04-04 15:38:56 +01:00
- ` buildProviderToolCompatFamilyHooks("gemini")` wires Gemini schema
cleanup + diagnostics for providers that need Gemini-safe tool schemas.
2026-04-04 15:53:24 +01:00
- ` normalizeGeminiToolSchemas(...)` and ` inspectGeminiToolSchemas(...)`
are the underlying public Gemini schema helpers.
2026-04-04 15:38:56 +01:00
- ` resolveXaiModelCompatPatch()` returns the bundled xAI compat patch:
` toolSchemaProfile: "xai"`, unsupported schema keywords, native
` web_search` support, and HTML-entity tool-call argument decoding.
- ` applyXaiModelCompat(model)` applies that same xAI compat patch to a
resolved model before it reaches the runner.
Real bundled example: the xAI plugin uses ` normalizeResolvedModel` plus
` contributeResolvedModelCompat` to keep that compat metadata owned by the
provider instead of hardcoding xAI rules in core.
2026-04-04 12:52:55 +01:00
The same package-root pattern also backs other bundled providers:
- ` @openclaw/openai -provider`: ` api.ts` exports provider builders,
default-model helpers, and realtime provider builders
- ` @openclaw/openrouter -provider`: ` api.ts` exports the provider builder
plus onboarding/config helpers
2026-03-22 11:35:53 -07:00
<Tabs>
<Tab title="Token exchange">
For providers that need a token exchange before each inference call:
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
` ``typescript
prepareRuntimeAuth: async (ctx) => {
const exchanged = await exchangeToken(ctx.apiKey);
return {
apiKey: exchanged.token,
baseUrl: exchanged.baseUrl,
expiresAt: exchanged.expiresAt,
};
2026-03-22 08:50:43 -07:00
},
2026-03-22 11:35:53 -07:00
` ``
</Tab>
<Tab title="Custom headers">
For providers that need custom request headers or body modifications:
` ``typescript
// wrapStreamFn returns a StreamFn derived from ctx.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);
};
},
` ``
</Tab>
2026-04-04 04:25:36 +01:00
<Tab title="Native transport identity">
For providers that need native request/session headers or metadata on
generic HTTP or WebSocket transports:
` ``typescript
resolveTransportTurnState: (ctx) => ({
headers: {
"x-request-id": ctx.turnId,
},
metadata: {
session_id: ctx.sessionId ?? "",
turn_id: ctx.turnId,
},
}),
resolveWebSocketSessionPolicy: (ctx) => ({
headers: {
"x-session-id": ctx.sessionId ?? "",
},
degradeCooldownMs: 60_000,
}),
` ``
</Tab>
2026-03-22 11:35:53 -07:00
<Tab title="Usage and billing">
For providers that expose usage/billing data:
` ``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);
},
` ``
</Tab>
</Tabs>
2026-03-22 18:56:49 -07:00
<Accordion title="All available provider hooks">
2026-03-22 11:35:53 -07:00
OpenClaw calls hooks in this order. Most providers only use 2-3:
| # | Hook | When to use |
| --- | --- | --- |
| 1 | ` catalog` | Model catalog or base URL defaults |
2026-04-04 07:46:02 +01:00
| 2 | ` applyConfigDefaults` | Provider-owned global defaults during config materialization |
2026-04-04 21:39:18 +01:00
| 3 | ` normalizeModelId` | Legacy/preview model-id alias cleanup before lookup |
| 4 | ` normalizeTransport` | Provider-family ` api` / ` baseUrl` cleanup before generic model assembly |
| 5 | ` normalizeConfig` | Normalize ` models.providers.<id>` config |
| 6 | ` applyNativeStreamingUsageCompat` | Native streaming-usage compat rewrites for config providers |
| 7 | ` resolveConfigApiKey` | Provider-owned env-marker auth resolution |
| 8 | ` resolveSyntheticAuth` | Local/self-hosted or config-backed synthetic auth |
| 9 | ` shouldDeferSyntheticProfileAuth` | Lower synthetic stored-profile placeholders behind env/config auth |
| 10 | ` resolveDynamicModel` | Accept arbitrary upstream model IDs |
| 11 | ` prepareDynamicModel` | Async metadata fetch before resolving |
| 12 | ` normalizeResolvedModel` | Transport rewrites before the runner |
2026-04-04 21:48:41 +01:00
Runtime fallback notes:
- ` normalizeConfig` checks the matched provider first, then other
hook-capable provider plugins until one actually changes the config.
If no provider hook rewrites a supported Google-family config entry, the
bundled Google config normalizer still applies.
- ` resolveConfigApiKey` uses the provider hook when exposed. The bundled
` amazon-bedrock` path also has a built-in AWS env-marker resolver here,
even though Bedrock runtime auth itself still uses the AWS SDK default
chain.
2026-04-04 21:39:18 +01:00
| 13 | ` contributeResolvedModelCompat` | Compat flags for vendor models behind another compatible transport |
| 14 | ` capabilities` | Legacy static capability bag; compatibility only |
| 15 | ` normalizeToolSchemas` | Provider-owned tool-schema cleanup before registration |
| 16 | ` inspectToolSchemas` | Provider-owned tool-schema diagnostics |
| 17 | ` resolveReasoningOutputMode` | Tagged vs native reasoning-output contract |
| 18 | ` prepareExtraParams` | Default request params |
| 19 | ` createStreamFn` | Fully custom StreamFn transport |
| 20 | ` wrapStreamFn` | Custom headers/body wrappers on the normal stream path |
| 21 | ` resolveTransportTurnState` | Native per-turn headers/metadata |
| 22 | ` resolveWebSocketSessionPolicy` | Native WS session headers/cool-down |
| 23 | ` formatApiKey` | Custom runtime token shape |
| 24 | ` refreshOAuth` | Custom OAuth refresh |
| 25 | ` buildAuthDoctorHint` | Auth repair guidance |
| 26 | ` matchesContextOverflowError` | Provider-owned overflow detection |
| 27 | ` classifyFailoverReason` | Provider-owned rate-limit/overload classification |
| 28 | ` isCacheTtlEligible` | Prompt cache TTL gating |
| 29 | ` buildMissingAuthMessage` | Custom missing-auth hint |
| 30 | ` suppressBuiltInModel` | Hide stale upstream rows |
| 31 | ` augmentModelCatalog` | Synthetic forward-compat rows |
| 32 | ` isBinaryThinking` | Binary thinking on/off |
| 33 | ` supportsXHighThinking` | ` xhigh` reasoning support |
| 34 | ` resolveDefaultThinkingLevel` | Default ` /think` policy |
| 35 | ` isModernModelRef` | Live/smoke model matching |
| 36 | ` prepareRuntimeAuth` | Token exchange before inference |
| 37 | ` resolveUsageAuth` | Custom usage credential parsing |
| 38 | ` fetchUsageSnapshot` | Custom usage endpoint |
| 39 | ` createEmbeddingProvider` | Provider-owned embedding adapter for memory/search |
| 40 | ` buildReplayPolicy` | Custom transcript replay/compaction policy |
| 41 | ` sanitizeReplayHistory` | Provider-specific replay rewrites after generic cleanup |
| 42 | ` validateReplayTurns` | Strict replay-turn validation before the embedded runner |
| 43 | ` onModelSelected` | Post-selection callback (e.g. telemetry) |
2026-03-22 11:35:53 -07:00
2026-04-05 14:05:35 +01:00
Prompt tuning note:
- ` resolveSystemPromptContribution` lets a provider inject cache-aware
system-prompt guidance for a model family. Prefer it over
` before_prompt_build` when the behavior belongs to one provider/model
family and should preserve the stable/dynamic cache split.
2026-03-22 11:35:53 -07:00
For detailed descriptions and real-world examples, see
[Internals: Provider Runtime Hooks](/plugins/architecture#provider-runtime-hooks).
</Accordion>
</Step>
<Step title="Add extra capabilities (optional)">
2026-03-30 01:21:00 +02:00
<a id="step-5-add-extra-capabilities"></a>
2026-04-04 19:59:50 +01:00
A provider plugin can register speech, realtime transcription, realtime
voice, media understanding, image generation, video generation, web fetch,
and web search alongside text inference:
2026-03-22 11:35:53 -07:00
` ``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 data */),
outputFormat: "mp3",
fileExtension: ".mp3",
voiceCompatible: false,
}),
});
2026-04-04 12:04:37 +09:00
api.registerRealtimeTranscriptionProvider({
id: "acme-ai",
label: "Acme Realtime Transcription",
isConfigured: () => true,
createSession: (req) => ({
connect: async () => {},
sendAudio: () => {},
close: () => {},
isConnected: () => true,
}),
});
api.registerRealtimeVoiceProvider({
id: "acme-ai",
label: "Acme Realtime Voice",
isConfigured: ({ providerConfig }) => Boolean(providerConfig.apiKey),
createBridge: (req) => ({
connect: async () => {},
sendAudio: () => {},
setMediaTimestamp: () => {},
submitToolResult: () => {},
acknowledgeMark: () => {},
close: () => {},
isConnected: () => true,
}),
});
2026-03-22 11:35:53 -07:00
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 */ }),
});
2026-04-04 19:47:30 +01:00
api.registerVideoGenerationProvider({
id: "acme-ai",
label: "Acme Video",
capabilities: {
2026-04-06 13:25:45 +01:00
generate: {
maxVideos: 1,
maxDurationSeconds: 10,
supportsResolution: true,
},
imageToVideo: {
enabled: true,
maxVideos: 1,
maxInputImages: 1,
maxDurationSeconds: 5,
},
videoToVideo: {
enabled: false,
},
2026-04-04 19:47:30 +01:00
},
generateVideo: async (req) => ({ videos: [] }),
});
2026-04-04 19:59:50 +01:00
api.registerWebFetchProvider({
id: "acme-ai-fetch",
label: "Acme Fetch",
hint: "Fetch pages through Acme's rendering backend.",
envVars: ["ACME_FETCH_API_KEY"],
placeholder: "acme-...",
signupUrl: "https://acme.example.com/fetch",
credentialPath: "plugins.entries.acme.config.webFetch.apiKey",
getCredentialValue: (fetchConfig) => fetchConfig?.acme?.apiKey,
setCredentialValue: (fetchConfigTarget, value) => {
const acme = (fetchConfigTarget.acme ??= {});
acme.apiKey = value;
},
createTool: () => ({
description: "Fetch a page through Acme Fetch.",
parameters: {},
execute: async (args) => ({ content: [] }),
}),
});
2026-04-04 19:47:30 +01:00
api.registerWebSearchProvider({
id: "acme-ai-search",
label: "Acme Search",
search: async (req) => ({ content: [] }),
});
2026-03-22 11:35:53 -07:00
}
` ``
OpenClaw classifies this as a **hybrid-capability** plugin. This is the
recommended pattern for company plugins (one plugin per vendor). See
[Internals: Capability Ownership](/plugins/architecture#capability-ownership-model).
2026-04-06 13:25:45 +01:00
For video generation, prefer the mode-aware capability shape shown above:
2026-04-06 15:24:16 +01:00
` generate`, ` imageToVideo`, and ` videoToVideo`. Flat aggregate fields such
as ` maxInputImages`, ` maxInputVideos`, and ` maxDurationSeconds` are not
enough to advertise transform-mode support or disabled modes cleanly.
Music-generation providers should follow the same pattern:
` generate` for prompt-only generation and ` edit` for reference-image-based
generation. Flat aggregate fields such as ` maxInputImages`,
` supportsLyrics`, and ` supportsFormat` are not enough to advertise edit
support; explicit ` generate` / ` edit` blocks are the expected contract.
2026-04-06 13:25:45 +01:00
2026-03-22 11:35:53 -07:00
</Step>
<Step title="Test">
2026-03-30 01:21:00 +02:00
<a id="step-6-test"></a>
2026-03-22 11:35:53 -07:00
` ``typescript src/provider.test.ts
import { describe, it, expect } from "vitest";
// Export your provider config object from index.ts or a dedicated file
import { 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();
});
});
` ``
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
</Step>
</Steps>
2026-03-22 08:50:43 -07:00
2026-03-29 11:59:19 -07:00
## Publish to ClawHub
Provider plugins publish the same way as any other external code plugin:
` ``bash
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
` ``
Do not use the legacy skill-only publish alias here; plugin packages should use
` clawhub package publish`.
2026-03-22 11:35:53 -07:00
## File structure
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
` ``
2026-03-29 09:09:56 +01:00
<bundled-plugin-root>/acme-ai/
2026-03-22 11:35:53 -07:00
├── 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)
` ``
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
## Catalog order reference
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
` catalog.order` controls when your catalog merges relative to built-in
providers:
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
| 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) |
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
## Next steps
2026-03-22 08:50:43 -07:00
2026-03-22 11:35:53 -07:00
- [Channel Plugins](/plugins/sdk-channel-plugins) — if your plugin also provides a channel
- [SDK Runtime](/plugins/sdk-runtime) — ` api.runtime` helpers (TTS, search, subagent)
- [SDK Overview ](/plugins/sdk-overview ) — full subpath import reference
- [Plugin Internals ](/plugins/architecture#provider-runtime-hooks ) — hook details and bundled examples