* fix(clawrouter): expose and forward catalog-backed Fast tiers * fix(clawrouter): validate tiers after caller payload overrides * fix(clawrouter): Fast unavailable for supported models OpenClaw-Publication: 5526e3ad-1835-4129-9cb8-528c5f29e4f0
36 KiB
| summary | title | sidebarTitle | read_when | |||
|---|---|---|---|---|---|---|
| Step-by-step guide to building a model provider plugin for OpenClaw | Building provider plugins | 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.
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
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 the prepared request until
login finishes. Use createProviderLoginFlowRegistry and
reserveProviderLoginFlow to reserve only the credential exchange.
After login finishes, call offerProviderLoginModelAccess with flows, flowKey,
prepared, and terminalMessage. Deliver its structured reply. Always release
the login in finally with releaseProviderLoginFlow({ flows, flowKey, record }).
The pending model-access question has its own lifetime and remains answerable
after that release; it does not block another login.
Pass the later command to answerProviderLoginModelAccess with flows, flowKey,
agentId, command, runtime, readConfig, and assertCurrent; signal is
optional. readConfig must return the host's current config. The owner validates
the answer, applies the choice, and consumes that question. Expired or conflicting
choices receive a fresh question based on current restrictions.
Use cancelProviderLoginFlow({ flows, flowKey }) to cancel either pending phase.
Do not reconstruct a wildcard write from button text or reuse a prepared request
for a new login.
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.
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
### Step 1: 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"],
"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"
}
}
}
```
```json 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
}
}
```
</CodeGroup>
`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.
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.
```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,
},
],
},
};
},
},
});
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,
import `findNormalizedProviderValue` and `resolveAuthProfileOrder` from
`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](/plugins/sdk-migration/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/agent-runtime`. 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](/plugins/sdk-provider-plugins/model-catalogs).
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.
Most providers only need `catalog` + `resolveDynamicModel`. Add hooks
incrementally as your provider requires them.
Start with the shared family builders in [Provider hook
families](/plugins/sdk-provider-plugins/hook-families), then wire individual
hooks with [Provider hook wiring](/plugins/sdk-provider-plugins/runtime-hooks).
### 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](/plugins/architecture#capability-ownership-model).
Register the audio capabilities from [Provider voice
capabilities](/plugins/sdk-provider-plugins/voice-and-audio). Register
embeddings, generation, fetch, and search from [Provider media and
search](/plugins/sdk-provider-plugins/media-and-search).
### Step 6: 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();
});
});
```
Publish to ClawHub
Provider plugins publish the same way as any other external code plugin:
clawhub package publish your-org/your-plugin --dry-run
clawhub 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
<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.
Provider stream decorators
wrapStreamFn receives optional request context for provider-owned behavior:
modelParamscontains only per-model params, with the current agent's model params taking precedence over default model params. Global and agent-wide params are excluded;extraParamsstill contains the merged request params.modelCallUrgencyis the host'sforeground,normal, orbackgroundclassification. 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.
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
- Channel Plugins - if your plugin also provides a channel
- SDK Runtime -
api.runtimehelpers (TTS, search, subagent) - SDK Overview - full subpath import reference
- Plugin Internals - hook details and bundled examples
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
Provider media and search — Embeddings, image and video generation, web fetch, and web search capabilities.