Live model discovery
If your provider exposes an OpenAI-compatible/models API, opt the
single-provider helper into shared discovery:
liveModelDiscovery: true is a public Plugin SDK contract with these
behaviors:
Relative catalog cache TTLs start when a successful load completes. Cache hits
preserve that deadline, and explicit absolute provider deadlines remain unchanged.
Pending loads retain their initial expiry so stalled work can be replaced.
Bundled providers set
discoveryMode: "strict" in their catalog options.
This code option keeps successful empty results empty and reports failed
acquisition through ProviderCatalogResult.outcomes, rather than returning
seed models as a successful refresh. HTTP 401/403 produces a catalog-scoped
auth-rejected outcome; other acquisition failures produce unavailable.
Neither a static catalog nor skipped discovery produces a live outcome.
Each outcome carries the profile selected for the actual request, when one
supplied its credential. Family providers report each sibling independently.
With a positive cache lifetime, validated empty results use the same
successful-observation lifetime as nonempty results. After expiry, ordinary
catalog reads return retained rows while the existing inventory owner refreshes
the provider in the background. ttlMs: 0 still disables response caching and
does not record an expiry for this renewal path.
Public metadata requests declare authentication: "none" in discovery
options. The prepared request then has no credential or profile identity;
its cache key is independent of the configured inference credential.
The returned provider configuration still retains its inference credential.
External calls that omit discoveryMode retain the advisory contract above.
The public Chutes, Hugging Face, KiloCode, and Vercel AI Gateway discovery
functions and builders also retain that default. Their bundled catalog hooks
pass { discoveryMode: "strict" } explicitly; Hugging Face discovery accepts
this options object after its existing timeout argument. The Chutes public
default retains its anonymous retry after HTTP 401; strict calls never retry
without the selected credential.
The strict and advisory paths share the same guarded transport and cache, with
separate cache identities. Advisory calls still retain only nonempty results.
Custom live builders can use runLiveProviderCatalog at their catalog hook
to convert acquisition errors into outcomes. Keep metadata-feed fallback
separate from account discovery; do not retry a rejected account request
anonymously or substitute seed rows inside a strict builder.
Custom catalog hooks may receive optional mode metadata from
ctx.resolveProviderApiKey(): api_key, oauth, or token. When present,
it describes that lookup’s selected credential. Use it when choosing a vendor
authentication scheme; a separate resolveProviderAuth() call may select a
different profile. Omitted mode metadata does not change existing callback behavior.
ctx.resolveProviderAuth() may set preparationFailed: true when OAuth
preparation exhausted its candidates. Do not treat that flag as absent
configuration or restart resolution of the same profiles. A hook may still
choose another credential source. Its returned provider configuration or
explicit outcome remains authoritative; otherwise the catalog owner reports
the consumed preparation failure with the attempted profile identities.
For a non-Bearer or nonstandard list endpoint, pass options instead of
true:
endpointUrl as an unconditional alternate host. Its
requireBaseUrl check is the credential-isolation boundary for providers
whose model-list host differs from their inference host.
If the provider needs custom model semantics rather than the conservative
OpenAI-compatible projection, keep only that projection in the plugin. Pass
it as projectRows; the shared runtime still owns guarded fetches,
provider-auth headers, cache admission, and static fallback.
Use buildLiveModelProviderConfig when the live API only tells you which
provider-owned static catalog rows are currently available:
index.ts
run should stay auth-gated and return null when no usable credential is
available. Keep an offline staticRun or static fallback so setup, docs,
tests, and picker surfaces do not depend on live network access. Use a TTL
appropriate for model-list freshness, avoid request-time filesystem polling,
and pass a provider-specific readRows / readModelId only when the
upstream response is not an OpenAI-compatible { data: [{ id, object }] }
shape.
During model-runtime preparation, staticCatalog.run and prepareSyntheticAuth
receive an optional signal. Shutdown and plugin/config replacement abort it.
Stop awaited acquisition when it aborts and finish resource cleanup before the
hook settles. OpenClaw discards cancelled results and joins cleanup before a
replacement can acquire the same agent resources.
For a separate authoritative metadata feed, the same
provider-catalog-live-runtime subpath exposes ProviderCatalogSnapshot:
each entry pairs a runtime model with its lifecycle status.
projectUpstreamProviderCatalogSnapshot rebuilds that snapshot from a
trusted seed and accepted upstream rows, dropping withdrawn upstream-only
models. projectProviderCatalogSnapshotRows intersects advertised IDs with
active snapshot entries, deduplicating in endpoint order;
listProviderCatalogSnapshotEntries projects the same lifecycle facts for
catalog consumers. Keep seed lifecycle policy and model-specific decoration
in the owning plugin. Derive static fallback eligibility after refreshing
metadata so the first failed or fully filtered discovery uses current status.
Public metadata never establishes account entitlement or expands the
credential scope of discovery.
The private createUpstreamProviderCatalog helper keeps this snapshot lifecycle in one prepared
owner. Supply the trusted seed, provider routes, metadata and model-list
endpoints, static-entry eligibility, and any model decoration. An optional
upstreamSeed controls which seed lifecycle facts survive an upstream refresh.
The owner exposes getSnapshot, refreshMetadata, buildStaticProvider, and
buildLiveProvider; credentials belong to each build call. Live builds refresh
metadata before deriving static eligibility and intersecting advertised IDs.
Metadata acquisition failure retains the previous snapshot; model-list failures
and empty results remain strict. refreshMetadata returns undefined when the
feed lacks the provider, so explicit model preparation cannot mistake retained
metadata for a successful refresh. Plugin policy still owns which models may
resolve directly from the seed or current snapshot.
Upstream reasoning metadata preserves omitted controls as unspecified and an
empty options or effort list as no effort control. A native null effort maps
to none; provider-native effort names retain their casing. These facts remain
separate from whether a model performs reasoning internally.
Official plugins use the private, pure
openclaw/plugin-sdk/model-catalog-pricing runtime subpath. It exposes
normalizeModelPricingCatalog(rows, normalizePricing, options?) for
provider-owned pricing feeds. It returns a map of complete costs: absent
prices are omitted, while malformed declared prices, invalid or duplicate
model IDs, and a feed with no usable prices return undefined. Supply the
provider’s unit conversion. Options can select readModelId(model) (default
model.id), readPricing(model) (default model.pricing), and
isSupportedPricing(rawPricing) (default true). Declared prices are
normalized and validated before unsupported schedules are omitted; duplicate
IDs are rejected even on unpriced or unsupported rows. Non-token domains
can return undefined from readPricing. No auth, discovery, or runtime
loader is imported.
DeepInfra’s pricing-api.ts uses these selectors for its native array and
model_name identities. Release plugins using the options contract (including
DeepInfra and Venice) with a matching host, and coordinate their plugin API
and minimum-host floors at release time. The private subpath is not an
independently versioned third-party compatibility API.
This subpath also exposes normalizeOpenRouterModelPricing(pricing) for
native OpenRouter pricing objects. It converts per-token rates and static
prompt-length overrides into a complete per-million cost schedule, without
network access or prices from another source. Overrides apply strictly above
min_prompt_tokens, counting uncached input, cache reads, and cache writes.
Matching entries apply in source order: later entries win per price key,
including at equal thresholds; omitted keys inherit the native base or an
earlier matching entry. Cache rates absent from the base default to zero.
Invalid effective token rates return undefined. Entries with time-based or
unknown conditions are skipped; other known charge dimensions are ignored.
Selecting catalog augmentation hooks
augmentModelCatalogWithProviderPlugins is exported from
openclaw/plugin-sdk/provider-catalog-runtime. Its optional top-level
providerIds selects which registered augmentModelCatalog hooks run:
- Omit
providerIdsto keep the unscoped behavior. - Pass
[]to run no augmentation hooks. - Pass provider IDs or registered aliases to select matching hooks. Matching normalizes IDs and aliases, including hook aliases used by provider families.
context.entries.
The shipped v2026.9.4 export has no providerIds selector and may ignore that
option at runtime. Plugins that depend on scoped hook selection must require a
host release containing the selector in openclaw.compat.pluginApi. Omitting
the option retains the existing behavior on both older and newer hosts.
This top-level selector is separate from the catalog.run callback context.
When ctx.providerIds is present, it contains the normalized provider
identities selected for that catalog owner. Return null before resolving
credentials or making network requests when the hook serves none of them;
OpenClaw also filters returned identities to that scope. An absent scope
means the caller requested the full catalog.
If the upstream provider uses different control tokens than OpenClaw, add a
small bidirectional text transform instead of replacing the stream path:
input rewrites the final system prompt and text message content before
transport. output rewrites assistant text deltas and final text before
OpenClaw parses its own control markers or channel delivery.
For bundled providers that only register one text provider with API-key
auth plus a single catalog-backed runtime, prefer the narrower
defineSingleProviderPluginEntry(...) helper:
buildProvider is the live catalog path used when OpenClaw can resolve real
provider auth. It may perform provider-specific discovery. Use
buildStaticProvider only for offline rows that are safe to show before auth
is configured; it must not require credentials or make network requests.
OpenClaw’s models list --all display currently executes static catalogs
only for bundled provider plugins, with an empty config, empty env, and no
agent/workspace paths.
If your auth flow also needs to patch models.providers.*, aliases, and
the agent default model during onboarding, use the preset helpers from
openclaw/plugin-sdk/provider-onboard. For registered connection-only setup,
use createProviderConnectionPresetAppliers(...) with a lazy catalogModels
supplier. Ordinary setup writes connection facts, aliases, and a missing
primary without copying the built-in catalog into saved configuration.
Explicit models.mode: "replace" evaluates the supplier and merges generated
rows behind authored rows. Each setup result owns its generated model data.
Use createDefaultModelsConnectionPresetAppliers(...) when replace mode must
retain the existing required-default rule: add the supplied defaultModels
only when the configured provider lacks the selected defaultModelId.
applyProviderConnectionConfig(...) provides the catalog variant for auth
flows that resolve their endpoint or primary per invocation. These helpers
preserve authored rows, existing aliases and fallbacks. The auth flow still
owns any explicit default selection.
The existing public createDefaultModelPresetAppliers(...),
createDefaultModelsPresetAppliers(...), and
createModelCatalogPresetAppliers(...) retain their catalog-seeding behavior
in ordinary mode. The latter two also accept lazy model suppliers. A provider
with published config helpers can share one preset descriptor between its
existing helper and its new registered setup helper; migrate the registration
without silently changing the published helper’s contract.
Independently published plugins must require the host release containing these
helpers in openclaw.compat.pluginApi. The core release version-sync tool
updates that range with the paired release; these imports do not work on an
older host that lacks the helpers.
When a provider’s native endpoint supports streamed usage blocks on the
normal openai-completions transport, prefer the shared catalog helpers in
openclaw/plugin-sdk/provider-catalog-shared instead of hardcoding
provider-id checks. supportsNativeStreamingUsageCompat(...) and
applyProviderNativeStreamingUsageCompat(...) detect support from the
endpoint capability map, so native Moonshot/DashScope-style endpoints still
opt in even when a plugin is using a custom provider id.
The live discovery examples above cover /models-style provider APIs. Keep
that discovery inside catalog.run, gated on usable auth, and keep
staticRun network-free for offline catalog generation.
Official provider plugins that share credentials can use
resolveFirstProviderCatalogAuth(ctx.resolveProviderApiKey, providerIds) from
the private runtime openclaw/plugin-sdk/provider-catalog-shared subpath.
Keep provider precedence in the caller’s ordered IDs. The helper stops at
the first result with an apiKey or discoveryApiKey and returns that whole
result, preserving its profile and auth mode. An unresolved SecretRef marker
takes precedence over another provider’s live key; fields are never mixed
across accounts. It returns undefined when no provider has auth and
propagates lookup failures. Official plugin releases using this host export
must require a host version that provides it in their compat.pluginApi.