Building plugins

Building provider plugins

Build a provider plugin to add a model provider (LLM) to OpenClaw: a model catalog, API-key auth, and dynamic model resolution.

Acme AI is a fictional vendor used throughout this guide and its child pages. Helpers named fetchAcme* in the samples are placeholders for your own vendor API calls, not exported OpenClaw functions.

Import an existing credential during sign-in

An auth method can declare credentialImport with a migrationProviderId, an exact itemId, and a credentialKind (api_key, oauth, or token). models auth login asks that migration owner for an auth-only plan before starting interactive sign-in. --force, --profile-id, and --set-default skip import. --set-default uses the auth method's recommended model through the normal sign-in flow.

The migration plugin declares its ID in contracts.migrationProviders and can export buildMigrationProvider() from a top-level migration-provider-api.ts public artifact. Keep that entry lightweight. Bundled plugins and enabled installed plugins can supply it without replacing the running plugin registry. Explicitly disabled or denied migration owners cannot execute their artifacts. The existing bundled migration compatibility rules still apply.

The login caller selects only the declared auth item. Its details must contain the matching provider and credentialKind. A migrated result also supplies the saved profileId. The owner must honor cancellation, reread the selected source before persistence, and reject a changed credential. Login passes configPatchMode: "none" so import preserves model defaults and restrictions. Unavailable storage or an unusable matching OAuth profile continues to interactive sign-in. A matching account identity alone does not make expired credentials usable. A failed selected import stops the operation instead of silently starting a different login.

Handle model access after sign-in

Existing consumers of runModelsAuthLoginFlow from openclaw/plugin-sdk/provider-auth-login-flow-runtime must handle a selection after credentials are saved. When effective restrictions can hide the provider's models, the existing prompter.select receives these options:

Value Label Effect
all Show all <Provider> models Adds that provider's wildcard to the existing policy owner.
keep Keep current restrictions Leaves restrictions unchanged.

Render the supplied message and options, and return the selected option's value. Do not assume that every select call chooses a provider or auth method. Neither choice activates a new default model. No choice is requested when restrictions are absent or already allow the whole provider.

Canceling or rejecting this post-save selection does not undo saved credentials. The flow throws ProviderAuthConfigApplyError, which extends ProviderCredentialsSavedError; report that credentials were saved instead of treating it as a failed credential exchange. Cancellation at the selection leaves restrictions unchanged. A later application failure can leave the policy saved but not active in the running Gateway. Keep credential persistence and model visibility outcomes distinct.

Defer the choice to a later reply

For chat buttons, pass the synchronous onModelAccessRequested callback. It receives a PreparedProviderModelAccess request and replaces the post-save select call; it does not apply the choice. Retain that request with the current login record from createProviderLoginFlowRegistry and reserveProviderLoginFlow.

After login finishes, use offerProviderLoginModelAccess with the same record, the prepared request, and the login's completion message. Deliver its structured reply. Pass the subsequent command to answerProviderLoginModelAccess with the same registry and flow key. This owner validates the answer, applies the choice, returns the final reply, and releases the completed record. Do not reconstruct a wildcard write from the button text or reuse a prepared request for a new login. Release the record on cancellation or a terminal failure.

Keep hosted writes authorized

Hosted callers supply signal and assertCurrent to check the current login, sender authority, and selected provider/method before effects and after awaited work. An abort signal or matching login identifier alone is not current authorization. beforePersistentEffect remains the credential-persistence preparation callback. Browser authorization ends after the credential phase; the later model choice uses the current conversation or wizard authority.

For a deferred choice, pass the answering command's current authority check as answerProviderLoginModelAccess.assertCurrent. Use its config argument when supplied: it is the policy writer's current config. Otherwise read the host's current config. The original login callback does not authorize a later command. Let the shared owner report the visibility outcome: a saved policy is not proof that the running Gateway applied it.

Walkthrough

  • Package and manifest

    Step 1: Package and manifest

    package.json
    {"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"  }}}
    openclaw.plugin.json
    {"id": "acme-ai","name": "Acme AI","description": "Acme AI model provider","providers": ["acme-ai"],"modelSupport": {  "modelPrefixes": ["acme-"]},"setup": {  "providers": [    {      "id": "acme-ai",      "envVars": ["ACME_AI_API_KEY"]    }  ]},"providerAuthAliases": {  "acme-ai-coding": "acme-ai"},"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}}

    setup.providers[].envVars lets OpenClaw detect credentials without loading your plugin runtime. Add providerAuthAliases when a provider variant should reuse another provider id's auth. modelSupport is optional and lets OpenClaw auto-load your provider plugin from shorthand model ids like acme-large before runtime hooks exist. openclaw.compat and openclaw.build in package.json are required for ClawHub publishing (openclaw.compat.pluginApi and openclaw.build.openclawVersion are the two required fields. minGatewayVersion falls back to openclaw.install.minHostVersion when omitted).

    The version strings in the sample manifests are placeholders. Pin them to the OpenClaw release your plugin builds and tests against.

  • Register the provider

    A minimal text provider needs an id, label, auth, and catalog. catalog is the provider-owned runtime/config hook. It can call live vendor APIs and returns models.providers entries.

    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,                },              ],            },          };        },      },    });     api.registerModelCatalogProvider({      provider: "acme-ai",      kinds: ["text"],      liveCatalog: async (ctx) => {        const apiKey = ctx.resolveProviderApiKey("acme-ai").apiKey;        if (!apiKey) return null;        return [          {            kind: "text",            provider: "acme-ai",            model: "acme-large",            label: "Acme Large",            source: "live",          },        ];      },    });  },});

    registerModelCatalogProvider is the newer control-plane catalog surface for list/help/picker UI, covering text, voice, image_generation, video_generation, and music_generation rows. Keep vendor endpoint calls and response mapping in the plugin. OpenClaw owns the shared row shape, source labels, and help rendering.

    That is a working provider. Users can now run openclaw onboard --acme-ai-api-key <key> and select acme-ai/acme-large as their model.

    For provider-key lookup and selection from an already loaded auth store,

    openclaw/plugin-sdk/provider-auth. This keeps provider entrypoints from loading the full agent runtime just to select a credential. The deprecated agent-runtime exports remain available for compatibility. Use the narrower provider-auth route in new code. See the removal timeline for the dates and gates that govern deprecated surfaces named on this page and its child pages.

    A custom interactive auth method that mints a static token or API key can request protected persistence on its returned profile:

    typescript
    return {  profiles: [    {      profileId: "acme-ai:device",      credential: { type: "token", provider: "acme-ai", token },      secretStorage: {        kind: "store",        namePrefix: "ACME_AI_TOKEN",      },    },  ],};

    OpenClaw keeps the inline value only while staged validation runs. At the final persistence boundary it writes the value to the protected local store and saves a tokenRef or keyRef in the auth profile. namePrefix must be an uppercase environment-style name. OpenClaw adds a stable suffix derived from the provider and final profile id so multiple profiles remain separate. Use this only for provider-minted static credentials, not rotating OAuth credentials or values already supplied as SecretRefs.

    For live /models discovery, catalog helpers, pricing normalization, and the narrower single-provider entry point, see Provider model catalogs.

  • 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, return the requested model directly from prepareDynamicModel. OpenClaw applies the same configured overrides and normalization as synchronous dynamic resolution. Existing hooks that return nothing still retry resolveDynamicModel after preparation.

  • Add runtime hooks (as needed)

    Most providers only need catalog + resolveDynamicModel. Add hooks incrementally as your provider requires them.

    Start with the shared family builders in Provider hook families, then wire individual hooks with Provider hook wiring.

  • Add extra capabilities (optional)

    Step 5: Add extra capabilities

    A provider plugin can register embeddings, speech, realtime transcription, realtime voice, media understanding, image generation, video generation, web fetch, and web search alongside text inference. OpenClaw classifies this as a hybrid-capability plugin - the recommended pattern for company plugins (one plugin per vendor). See Internals: Capability Ownership.

    Register the audio capabilities from Provider voice capabilities. Register embeddings, generation, fetch, and search from Provider media and search.

  • Test

    Step 6: Test

    src/provider.test.ts
    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

    Provider plugins publish the same way as any other external code plugin:

    bash
    clawhub package publish your-org/your-plugin --dry-runclawhub package publish your-org/your-plugin

    clawhub skill publish <path> is a different command for publishing a skill folder, not a plugin package - do not use it here.

    File structure

    Code
    <bundled-plugin-root>/acme-ai/├── package.json              # openclaw.providers metadata├── openclaw.plugin.json      # Manifest with provider auth metadata├── 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

    Where each section moved

    Every section of the single-page version now lives on this page or on one of the five child pages below. The anchors from the single-page version still resolve here.

    Provider model catalogs

    Provider model catalogs — Live model discovery, catalog helpers, pricing normalization, and the single-provider entry helper.

    Provider hook families

    Provider hook families — Shared replay, stream, and tool-compat family builders and the SDK seams behind them.

    Provider hook wiring

    Provider hook wiring — Per-hook wiring for auth exchange, headers, transport identity, usage, and the hook order table.

    Provider voice capabilities

    Provider voice capabilities — Speech, realtime transcription, realtime voice, and media understanding capabilities.

    Provider media and search — Embeddings, image and video generation, web fetch, and web search capabilities.

    Was this useful?
    On this page

    On this page