Files
openclaw/docs/plugins/sdk-provider-plugins.md
Vincent Koc 28838802d4 docs(plugins): add SDK reference and how-to guide pages (#52366)
* docs(plugins): add SDK reference and how-to guide pages

Create 7 new plugin SDK documentation pages:
- sdk-overview: import map, registration API reference
- sdk-entrypoints: definePluginEntry/defineChannelPluginEntry reference
- sdk-runtime: api.runtime namespace reference
- sdk-setup: packaging, manifests, config schemas reference
- sdk-channel-plugins: step-by-step channel plugin how-to
- sdk-provider-plugins: step-by-step provider plugin how-to
- sdk-testing: test utilities and patterns reference

Restructure plugin docs navigation with nested groups:
- Top-level: user-facing pages (Install, Community, Bundles)
- Building Plugins: Getting Started, Channel, Provider
- SDK Reference: Overview, Entry Points, Runtime, Setup, Testing, Migration, Manifest, Internals

Revise existing pages for new IA:
- building-plugins.md: tightened as quick-start, routes to detailed guides
- architecture.md: updated info box with links to new guides
- sdk-migration.md: expanded Related section

* docs(plugins): add Mintlify components (Steps, CodeGroup, Tabs, Accordion, CardGroup)

- Channel plugin guide: wrap walkthrough in Steps, use CodeGroup for
  package.json/manifest, Accordion for createChatChannelPlugin details,
  CardGroup for advanced topics
- Provider plugin guide: wrap walkthrough in Steps, use CodeGroup for
  package files, Tabs for hook examples, Accordion for all-hooks reference
- Getting started: use CardGroup for plugin-type picker and next steps,
  CodeGroup for package/manifest
- SDK Overview: wrap subpath tables in AccordionGroup for scannability

* fix(docs): address PR review feedback on plugin SDK pages

- Remove nonexistent api.runtime.channel.handleInboundMessage call,
  replace with realistic webhook pattern and note about channel-specific
  inbound handling (issue a)
- Fix registrationMode values: 'setup' -> 'setup-only' and 'setup-runtime'
  matching actual PluginRegistrationMode type (issue b)
- Fix createOptionalChannelSetupSurface params: channelId -> channel,
  add required label field (issue c)
- Fix broken anchor links: #multi-capability-providers ->
  #step-5-add-extra-capabilities, #plugin-kinds -> #registration-api (issue d)
- Add missing acmeChatApi import in channel plugin example (issue e)
- Fix undefined provider variable in provider test example (issue f)

* fix(docs): use correct createProviderApiKeyAuthMethod options

Replace incorrect params (provider, validate) with actual required fields
(providerId, methodId, optionKey, flagName, promptMessage) matching
src/plugins/provider-api-key-auth.ts.

* fix(docs): address second round of PR review feedback

- Add required model fields (reasoning, input, cost, contextWindow,
  maxTokens) to catalog example (issue b)
- Fix buildChannelConfigSchema to take a Zod schema argument (issue c)
- Replace fabricated setupWizard steps/run with real ChannelSetupWizard
  contract (channel, status, credentials) (issue d)
- Add required sessionFile/workspaceDir to runEmbeddedPiAgent (issue e)
- Fix wrapStreamFn to return StreamFn from ctx.streamFn (issue f)
2026-03-22 11:35:53 -07:00

371 lines
12 KiB
Markdown

---
title: "Building Provider Plugins"
sidebarTitle: "Provider Plugins"
summary: "Step-by-step guide to building a model provider plugin for OpenClaw"
read_when:
- 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
---
# 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>
<Step title="Package and manifest">
<CodeGroup>
```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
}
}
```
</CodeGroup>
The manifest declares `providerAuthEnvVars` so OpenClaw can detect
credentials without loading your plugin runtime.
</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",
}),
],
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 <key>` and select
`acme-ai/acme-large` as their model.
</Step>
<Step title="Add dynamic model resolution">
If your provider accepts arbitrary model IDs (like a proxy or router),
add `resolveDynamicModel`:
```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,
}),
});
```
If resolving requires a network call, use `prepareDynamicModel` for async
warm-up — `resolveDynamicModel` runs again after it completes.
</Step>
<Step title="Add runtime hooks (as needed)">
Most providers only need `catalog` + `resolveDynamicModel`. Add hooks
incrementally as your provider requires them.
<Tabs>
<Tab title="Token exchange">
For providers that need a token exchange before each inference call:
```typescript
prepareRuntimeAuth: async (ctx) => {
const exchanged = await exchangeToken(ctx.apiKey);
return {
apiKey: exchanged.token,
baseUrl: exchanged.baseUrl,
expiresAt: exchanged.expiresAt,
};
},
```
</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>
<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>
<Accordion title="All 21 available hooks">
OpenClaw calls hooks in this order. Most providers only use 2-3:
| # | Hook | When to use |
| --- | --- | --- |
| 1 | `catalog` | Model catalog or base URL defaults |
| 2 | `resolveDynamicModel` | Accept arbitrary upstream model IDs |
| 3 | `prepareDynamicModel` | Async metadata fetch before resolving |
| 4 | `normalizeResolvedModel` | Transport rewrites before the runner |
| 5 | `capabilities` | Transcript/tooling metadata |
| 6 | `prepareExtraParams` | Default request params |
| 7 | `wrapStreamFn` | Custom headers/body wrappers |
| 8 | `formatApiKey` | Custom runtime token shape |
| 9 | `refreshOAuth` | Custom OAuth refresh |
| 10 | `buildAuthDoctorHint` | Auth repair guidance |
| 11 | `isCacheTtlEligible` | Prompt cache TTL gating |
| 12 | `buildMissingAuthMessage` | Custom missing-auth hint |
| 13 | `suppressBuiltInModel` | Hide stale upstream rows |
| 14 | `augmentModelCatalog` | Synthetic forward-compat rows |
| 15 | `isBinaryThinking` | Binary thinking on/off |
| 16 | `supportsXHighThinking` | `xhigh` reasoning support |
| 17 | `resolveDefaultThinkingLevel` | Default `/think` policy |
| 18 | `isModernModelRef` | Live/smoke model matching |
| 19 | `prepareRuntimeAuth` | Token exchange before inference |
| 20 | `resolveUsageAuth` | Custom usage credential parsing |
| 21 | `fetchUsageSnapshot` | Custom usage endpoint |
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)">
A provider plugin can register speech, media understanding, image
generation, and web search alongside text inference:
```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,
}),
});
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](/plugins/architecture#capability-ownership-model).
</Step>
<Step title="Test">
```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();
});
});
```
</Step>
</Steps>
## File structure
```
extensions/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-in
providers:
| 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](/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