On this page

On this page

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.

Auth methods can return agentRuntimeId with defaultModel when sign-in belongs to a native harness. Model Setup applies that runtime to the selected agent's model entry, not to global model defaults.

ProviderAuthContext.agentId identifies the target agent when the caller has one. Native sign-in methods may use it to check previously remembered permissions. After explicit user consent, return ProviderAuthResult.nativeRuntimeConsent with the accepted harness IDs. Model Setup stages those permissions on that agent in the same verified config commit as the runtime routing and plugin enable. Do not write a separate permission store or infer consent from successful login.

Checklist before you open a provider PR

  • Live listing. If the vendor has a model-list API, the provider lists the account's models live, so new vendor models need no OpenClaw release. See live model discovery. src/plugins/contracts/provider-live-listing.contract.test.ts enforces this for bundled hosted providers.
  • Thinking from catalog data. Put the profile on the manifest row: compat.defaultThinkingLevel makes the row own it, and thinkingLevelMap, compat.supportedReasoningEfforts, compat.adaptiveThinking, compat.thinkingRequired and compat.thinkingLevelLabels shape the ladder. Do not add a resolveThinkingProfile hook for a model its row can describe. Add the row to src/plugins/contracts/catalog-thinking-profiles.contract.test.ts. Use compat.thinkingLevels when the offered ladder differs from what the map implies.
  • Live listing needs both halves. The entry sets catalog.liveModelDiscovery and the manifest declares modelCatalog.discovery: { "<provider>": "refreshable" }. Without the manifest half, the Gateway never refreshes the provider and only catalog rows appear.
  • Row IDs match the listing. Use the ID the vendor's model list returns. modelIdNormalization aliases only fold what users type.
  • Connection-only setup. Onboarding saves the endpoint and credential through createProviderConnectionPresetAppliers. Outside models.mode: "replace", which keeps its documented exception, it never copies catalog rows into config. See model catalogs.
  • Docs. Add docs/providers/<id>.md and list the provider in alphabetical order on the provider index.
  • Proof. Run a real agent turn on an isolated Gateway with a real key before the PR opens. API calls and mocked transports do not replace it. Make that Gateway look like a real install:
    • Enable the bundled plugins users run alongside yours, with credentials, not only your provider. Other plugins' hooks only show up when they are configured.
    • Save the key the way users do (the Control UI provider dialog, or openclaw models auth paste-api-key), not only through an environment variable.
    • Set OPENCLAW_DEBUG_MODEL_TRANSPORT=1 and check that the [model-fetch] start provider=<id> … url= line names your vendor's endpoint.
  • Stay inside your provider. Core also runs some hooks for providers you do not own:
    • normalizeTransport receives other providers' transports. Return nothing unless the context names your provider or already targets your endpoint. Core ignores another plugin's change that moves a provider to a different origin, and src/plugins/contracts/provider-transport-scope.contract.test.ts fails any bundled hook that rewrites a foreign transport.
    • augmentModelCatalog and resolveExternalAuthProfiles run as global discovery. Return only catalog rows and auth profiles for providers your plugin owns.
  • New model for an existing provider. Add catalog data, not code. Search every provider for the predecessor model ID before adding an exact-ID rule.

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.

Read a selected personal account

isUserModelAuthProfileId and withPersonalAuthProfileStore from openclaw/plugin-sdk/provider-auth-runtime route an explicitly selected personal account through its credential owner. The ID predicate recognizes a storage locator; it does not authorize access.

Inside the store callback, await owner.read() to prepare the selected credential. Before using that credential or disclosing an account-specific result, compare it with owner.readCurrentCredential(). This synchronous check uses owner-published facts without SQL. Reconnects, ownership changes, and unresolved writes invalidate the retained authority. Keep the asynchronous operation inside the callback and retain the caller's current-client and cancellation checks at each external effect.

For an agent or shared auth store, withAuthProfileStoreAtEffect on the same runtime surface retains the admitted read sources for its callback. It requires a live shared-state owner. Each read groups the selected sources into one worker request. Await reader.read() at disclosure to read current worker-owned rows, then compare the selected credential or account with the prepared request. This read bypasses runtime snapshots and the row cache. Call reader.assertCurrent() immediately before the effect to reject intervening owner writes without SQL. Keep caller authorization and cancellation checks there too; a prepared store alone does not authorize disclosure. The reader expires when the callback returns.

Loopback OAuth callbacks

Bundled providers use startProviderOAuthLoopbackCallbackServer from openclaw/plugin-sdk/provider-auth-runtime to bind their callback before opening the browser. waitForCallback() returns either an OAuth error or a validated code/state pair with parameters: URLSearchParams for provider-specific fields. Repeated parameters remain available for the provider to validate.

The default response acknowledges the callback and closes the listener. Set deferResponse: true to finish token exchange and identity checks before calling complete({ status, body, contentType }). Abort, optional timeoutMs, and browser disconnection still close a deferred response; a late complete() is a no-op. Always call close() in finally. The caller's signal and authority checks own token requests and persistence; the listener deadline does not cancel that work.

By default, the listener binds every loopback address resolved for the redirect hostname. bindHostname adds a loopback host. Use bindOnlyHostname instead to preserve a provider's exact Node bind host (localhost, 127.0.0.1, or ::1).

Declare physical credential ownership

Normal providers use agent-scoped profiles and configured provider credentials by default. A provider whose credentials are protected plugin-global settings can declare authScope: "plugin" on its canonical modelCatalog.providers entry and its runtime provider definition. These declarations must agree; unavailable or disabled executables must not turn a plugin credential request into an agent-profile lookup.

Plugin scope uses the selected live provider's synchronous resolveSyntheticAuth hook. Read the existing host-prepared plugin secret input there; do not resolve cold SecretRefs or copy them into agent auth stores. The common owner rechecks instance, hook identity and credential revision around that read. Cached/restored synthetic bearer results cannot replace live authority. Explicit or preferred profile pins are rejected before the hook, with no environment or models.providers fallback.

A declared no-auth marker establishes credential eligibility only. It does not establish service health, successful inference, or verified local model artifacts. Catalog readiness uses this same live source and treats missing prepared evidence as unknown or unavailable, never as an implicit profile grant. Existing agent-scoped providers retain their normal account precedence. Tool preparations that explicitly exclude plugin auth also exclude agent-credential fallback for plugin-scoped providers.

Handle model access after sign-in

Consumers of runModelsAuthLoginFlow from openclaw/plugin-sdk/provider-auth-login-flow-runtime get no model-access prompt. When restrictions hide every model of the provider, the flow adds <provider>/* to the restriction that governs the agent after credentials are saved, and returns modelAccess: { message, applied }. Show message with the completion text. applied: false means the policy was saved but the running Gateway has not confirmed it. Restrictions that already name some of the provider's models, and configurations without restrictions, stay unchanged.

A failed policy write 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. Use createProviderLoginFlowRegistry and reserveProviderLoginFlow to reserve the credential exchange, and release it in finally with releaseProviderLoginFlow({ flows, flowKey, record }).

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. The model-access write after sign-in uses the same assertCurrent check.

Submit JSON requests

Bundled Bearer-key providers use resolveProviderApiKeyHttpRequestConfig from openclaw/plugin-sdk/provider-http to resolve current credentials and HTTP request policy together. Pass req, provider, missingApiKeyMessage, and the usual HTTP configuration options. JSON content type is the default; set jsonContentType: false for multipart or other non-JSON requests. Provider-specific endpoints, extra headers, and request overrides remain plugin-owned.

Use postJsonResponse from openclaw/plugin-sdk/provider-http when a JSON POST returns a complete JSON response. It uses the guarded POST transport, checks the HTTP status, reads bounded JSON, and releases the transport before returning or throwing. Polling and generated-asset downloads keep their own request lifetimes.

Pass the usual postJsonRequest transport options plus errorLabel. Optional responseLabel changes the JSON parsing error label; it defaults to errorLabel. errorOptions and readOptions preserve the options accepted by assertOkOrThrowHttpError and readProviderJsonResponse, including deadlines, byte limits, and credential-redaction headers. Set objectResponse: true to require a JSON object; otherwise use the generic result type and validate the vendor response at the plugin boundary. Submission POSTs are not retried unless the caller explicitly supplies the existing retry options.

Keep postJsonRequest for streaming or binary responses, custom response/error parsers, and code that needs to retain the response transport. Those callers must release it in finally.

Use fetchWithProviderHttpGuard from openclaw/plugin-sdk/provider-http when the provider owns its request admission, response parsing, and release lifetime. It accepts guarded-fetch options and returns the response and release callback. Always release in finally. Eligible direct requests share the host's bounded provider connection pool after fresh DNS and SSRF validation on every request. Proxy and custom TLS routes retain their existing transport policy. Plugins do not retain or close the shared pool; the Gateway owns its shutdown.

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",          },        ];      },    });  },});

    Auth callbacks return an authoring configPatch that the host applies. A plugin-owned setup flow that applies its own result should use applyProviderAuthConfigPatch(config, result.configPatch, result) from openclaw/plugin-sdk/provider-auth. It preserves the host's merge semantics, including individual header deletions and replaceDefaultModels; do not shallow-spread an auth patch into runtime config.

    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.

    To read runtime auth profiles, await loadAuthProfileStoreForRuntimeAsync from openclaw/plugin-sdk/provider-auth. It reads persisted profiles in a database worker and preserves the optional third env argument for service-owned state directories. The synchronous loadAuthProfileStoreForRuntime is deprecated through 2026-12-31 and will be removed in the first January 2027 stable release.

    createProviderApiKeyAuthMethod accepts an optional validateApiKey callback. Return an error message to reject a resolved key before any auth profile is returned or saved. The check runs for interactive input, resolved SecretRefs, and non-interactive flag, environment, or saved-profile credentials.

    Bundled custom API-key methods can use captureProviderApiKey and persistProviderApiKey from openclaw/plugin-sdk/provider-auth-api-key when vendor prompts or validation need to stay between auth steps. captureProviderApiKey(ctx, options) accepts the existing token/provider, environment, and prompt options. It returns the resolved apiKey for validation alongside the original storage input and mode, without saving credentials. Build returned profiles from input and mode so SecretRefs remain references. The helper preserves the context's staged workspace and secret-storage prompt preference.

    persistProviderApiKey(ctx, profileId, { provider, resolved, metadata }) accepts an already resolved non-interactive key. It leaves profile-sourced credentials unchanged, returns false if credential conversion fails, and propagates persistence errors. Keep vendor checks before this call; apply auth-profile config and model defaults afterward through their existing owners. Neither helper chooses an endpoint or model. Interactive auth methods can use ctx.existingProfiles to reconnect with host-authorized { profileId, credential } candidates. CLI login and onboarding supply stored profiles for the selected provider; --profile-id narrows CLI login to that profile. Personal account flows supply only the current person's selected private account. An absent or empty list means no reusable account. Provider methods select compatible candidates and offer reuse or a new account; they must not load shared credentials to fill the list.

    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)

    Within each phase, OpenClaw runs up to four catalog hooks concurrently. It waits for the phase to settle, then merges model rows and provider outcomes in provider label order. Hooks retain their individual discovery deadlines; completion order does not change catalog precedence.

    Per-request credentials in custom transports

    createStreamFn receives an optional requestAuth capability with version: 1 and resolve({ signal }): Promise<{ apiKey: string; assertCurrent(): void }>. The embedded run's auth owner supplies it for a selected stored OAuth profile. Await it immediately before every physical request, including provider-private continuation and stream-recovery requests, then call the result's assertCurrent() and check cancellation again before dispatch. Do not cache the returned bearer or fall back to StreamOptions.apiKey on rejection.

    The capability re-reads the canonical credential owner and uses the existing OAuth refresh/rotation machinery. It stays bound to the selected profile, provider, account and automation authorization, and rejects after the run closes, the auth selection advances, or cancellation. Closure during awaited preparation also prevents returning credentials. An already-claimed OAuth refresh retains its existing independent settlement owner.

    This is additive: older hosts, other completion entrypoints, and calls without a selected stored OAuth profile may omit it. Bounded auth views such as openclaw agent exec with state-database shared auth, and env-only scopes, omit it because those views cannot supply canonical account authorization. Their existing local-credential and shared-OAuth exclusion rules remain unchanged. Setup-owned verification of a replacement OAuth sign-in also omits the capability: it uses the exact active setup credential scope, not ambient account authorization. The replacement stays unavailable outside that scope until verified activation. Runtime-only external OAuth profiles also omit the capability: the external credential source retains ownership, without requiring a canonical stored row. Persisted external profiles retain canonical capture and retirement checks. Absence retains the existing static apiKey behavior, not a guarantee of renewal during an attempt. Static API keys are unchanged. Account-less legacy OAuth credentials may keep the same refresh grant; a rotated grant without account identity fails closed rather than assuming it belongs to the same account. This capability does not retry HTTP 401, replay conversations, or transfer refresh grants to a transport.

    Provider stream decorators

    wrapStreamFn receives optional request context for provider-owned behavior:

    • modelParams contains only per-model params, with the current agent's model params taking precedence over default model params. Global and agent-wide params are excluded; extraParams still contains the merged request params.
    • modelCallUrgency is the host's foreground, normal, or background classification. Translate it into provider-native scheduling hints only when the selected endpoint supports them and the operator has enabled the behavior.

    A wrapper that only decorates requests can return its callable with preservesGenericCompatibility: true, for example Object.assign(decoratedStream, { preservesGenericCompatibility: true }). This keeps shared request shaping and output normalization active, including MiMo reasoning replay and final-answer handling. Without the flag, a replacement stream keeps the existing behavior of taking ownership of those compatibility policies. Returning the original stream or no wrapper also preserves them.

    Providers routing OpenAI Responses through a proxy can opt into buildProviderStreamFamilyHooks("openai-responses-service-tier") from openclaw/plugin-sdk/provider-stream-family. Apply it only after qualifying the upstream as OpenAI and the selected API as openai-responses, not merely because an endpoint is OpenAI-compatible. The family shares native Fast/Standard and service-tier precedence, samples timed Auto once per request, and preserves explicit payload tiers, provider identity, credentials, and cancellation. It does not attach native OpenAI headers or unrelated Responses defaults.

    ClawRouter qualifies these routes from the OpenAI provider's Responses catalog entries and preserves exact pricing.serviceTiers[].id values in private route metadata. Its lightweight picker policy and runtime hook share those facts: Fast requires advertised priority, and explicit Ultrafast requires ultrafast. Missing tier metadata never enables Fast by model name. Pricing tiers describe model capability, not key entitlement; the service remains authoritative and returns an unallowed-tier error without silently downgrading the request.

    Bundled adapters share wrapProviderPayloadHook and mapAssistantMessageStream through the existing private provider-stream-shared seam. Payload composition runs defaults before the caller hook and wire constraints after its result settles; each adapter keeps its own replacement/null policy. The stream mapper preserves synchronous returns without importing the embedded runner. These are bundled implementation helpers, not new third-party SDK contracts.

    The optional fourth argument to wrapProviderPayloadHook, inputObserver, defaults to false. Set it to true only when both policy callbacks preserve retained input items and their nested content. Settings-only policies can then reuse prepared input instead of cloning history. The wrapped caller hook still receives editable input when needed; its current requirements are checked per request, including live extension-handler registration and removal. This flag does not change callback order or the adapter's replacement/null policy.

    applyOpenAICompletionsToolReasoning shares the native tools/reasoning wire policy with pass-through adapters through that private seam. Callers qualify the endpoint and normalize vendor-prefixed model IDs before applying it.

    Bundled transports use recordSuppressedReasoning from that same private seam when a model marked reasoning: false returns thought fields. It records only their presence and returns an error for a completed response with no visible answer or tool call. Transports publish that error through their ordinary error terminal; the host owns per-session warning limits and status hints.

    Injected-client adapters also reuse the Anthropic thinking and output-cap owners through the private provider-transport-runtime seam. Client creation and provider-specific defaults stay with the adapter; the shared request builder owns sampling restrictions and cache-marker budgeting.

    Local provider transports can report a transient wait through the optional StreamOptions.onProviderWait callback. Pass "response" while waiting for a response, or "slot" only when backend evidence correlates the current request with a slot wait. Pass null to clear it. This is status metadata, never assistant text or evidence that cancellation released a backend slot.

    createProviderWaitObserver from openclaw/plugin-sdk/provider-transport-runtime provides the shared delayed status lifecycle. Call start() when dispatching the prepared request and stop() at the first response event and in cleanup. It emits the generic response wait after one second and clears it on cancellation or stop(). It creates no timer when the caller does not observe waits. A provider with request-correlated evidence can call report("slot"). Any backend probes must be bounded by that request's lifetime; do not add an always-on polling loop.

    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 interfaces 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.

    Worker resource receipts

    A worker provider may advertise supportsResourceReceipts: true on its existing WorkerProvider registration. This promises exact-resource handling in provision replay, inspection, and destruction; it does not advertise disk retention.

    WorkerResourceReceipt has version 1, the exact leaseId, and an identity map containing bounded, nonsecret immutable provider fields. The provider owns those fields; core validates their representation and preserves their equality, without interpreting vendor resource names. Use 1–16 ASCII identifier keys (up to 64 characters), with nonempty trimmed string values up to 2,048 characters and no control characters. Never include credentials or bearer URLs.

    During provisioning, await options.recordResourceReceipt(receipt) as soon as the allocated resource can be qualified, before setup, enrollment, or compensating cleanup affects that resource. Return the same receipt with the lease. The host commits it against the environment's original provision operation. Subsequent calls carry resourceReceipt: verify it against provider-owned facts, never replace it or allocate a successor. Inspection returns the same qualified receipt alongside status; an unavailable or mismatched observation throws. Even a destroyed observation cannot settle receipt-bound custody without that identity.

    Destruction checks assertCurrent after preparation and immediately before each provider effect, and resolves only after exact-resource deletion is confirmed. A rejected or lost deletion reply retains cleanup custody. The host can retry that exact deletion after restart even when live inspection labels are gone. Legacy providers and persisted rows with no receipt keep their existing contract; receipt-bearing rows refuse providers that no longer advertise this capability.

    Was this useful?