Translate vendor-specific LLM APIs into OpenClaw’s uniform provider contract for the agent engine.
Provider Adapters
How AI provider extensions connect models to the agent engine
Provider adapters implement the ProviderPlugin interface — a rich contract covering authentication, model discovery, transport normalization, stream processing, and capability declaration. Each provider extension (OpenAI, Anthropic, Ollama, etc.) builds one or more ProviderPlugin objects and registers them via api.registerProvider().
📚 What is this & why does it matter?
The ProviderPlugin interface is the abstraction layer that lets OpenClaw talk to any LLM — cloud-hosted or self-hosted — through a uniform streaming interface. Without it, adding a new AI model backend would require rewriting the agent engine. With it, you implement a handful of methods (auth, model resolution, transport normalization) and the engine handles the rest.
💡 Real-world analogy
Provider adapters are like power adapters for international travel. Every country (AI vendor) has a different plug shape (API format) and voltage (auth scheme). The adapter translates your device’s power needs (agent engine’s streaming interface) into the local standard. The agent engine doesn’t care whether it’s plugged into a 110V US outlet (OpenAI) or a 220V EU outlet (Anthropic) — the adapter handles conversion.
Contents
Overview
User config (API keys, base URLs), model selection, chat messages, tool schemas.
Auth method selection, dynamic model resolution, transport normalization, thinking-level support.
Streaming token responses, model catalogs, usage snapshots, attribution headers.
flowchart LR
subgraph Agent["Agent Engine"]
Req["Chat Request"]
Stream["Stream Consumer"]
end
subgraph Provider["ProviderPlugin"]
Auth["Auth Resolution"]
Model["Model Catalog"]
Transport["Transport Normalization"]
StreamFn["Stream Function"]
end
subgraph Vendor["Vendor API"]
API["HTTP Endpoint"]
end
Req --> Auth
Auth --> Model
Model --> Transport
Transport --> StreamFn
StreamFn --> API
API --> Stream
When the agent engine needs to send a chat request, it resolves the provider from the model ID, runs through the provider’s auth chain, normalizes the transport (base URL, API key headers), creates a streaming function, and pipes the response back to the conversation.
Decision Points
| Decision | What controls it | Why it matters |
|---|---|---|
| Which auth method to use | auth: ProviderAuthMethod[] array ordering | First matching auth method wins; determines API key source and config flow |
| Dynamic model resolution | resolveDynamicModel(ctx) | Allows forward-compatibility with unreleased models (e.g., gpt-5.4 before catalog update) |
| Transport override | normalizeTransport(ctx) | Redirects API calls through proxies, custom endpoints, or gateway overlays |
| Stream wrapping | wrapStreamFn(ctx) | Injects vendor-specific headers (attribution, tracing) without modifying the base stream |
| Thinking level support | supportsXHighThinking(ctx) | Enables extended reasoning for capable models (o-series, Claude 3.5+) |
| Model suppression | suppressBuiltInModel(ctx) | Hides deprecated or overlapping built-in models when provider takes over |
How It Works
The ProviderPlugin Interface
The ProviderPlugin type defines 25+ optional methods. Only id, label, and auth are required — everything else has sensible defaults:
type ProviderPlugin = {
// Identity
id: string; // Unique provider ID (e.g., "openai")
label: string; // Display name (e.g., "OpenAI")
docsPath?: string; // Path to documentation
envVars?: string[]; // Expected env vars (e.g., ["OPENAI_API_KEY"])
// Authentication
auth: ProviderAuthMethod[]; // Ordered list of auth strategies
// Model Discovery
catalog?: ProviderPluginCatalog;
resolveDynamicModel?: (ctx) => ProviderRuntimeModel | null;
augmentModelCatalog?: (ctx) => ModelCatalogEntry[];
// Transport
normalizeTransport?: (ctx) => { api?: string; baseUrl?: string } | null;
normalizeResolvedModel?: (ctx) => ProviderRuntimeModel | null;
normalizeConfig?: (ctx) => ModelProviderConfig | null;
// Streaming
createStreamFn?: (ctx) => StreamFn | null;
wrapStreamFn?: (ctx) => StreamFn | null;
// Capabilities
capabilities?: Partial<ProviderCapabilities>;
supportsXHighThinking?: (ctx) => boolean;
resolveDefaultThinkingLevel?: (ctx) => ThinkingLevel | null;
// Usage & Billing
resolveUsageAuth?: (ctx) => Promise<ProviderResolvedUsageAuth>;
fetchUsageSnapshot?: (ctx) => Promise<ProviderUsageSnapshot>;
// Error Messages
buildMissingAuthMessage?: (ctx) => string | null;
buildUnknownModelHint?: (ctx) => string | null;
};
Authentication Chain
Each provider declares one or more ProviderAuthMethod objects. The auth resolver tries them in order, returning the first that succeeds (API key present, OAuth token valid, etc.):
// From extensions/openai/openai-provider.ts
auth: [
createProviderApiKeyAuthMethod({
providerId: "openai",
methodId: "api-key",
label: "OpenAI API key",
hint: "Direct OpenAI API key",
optionKey: "openaiApiKey",
flagName: "--openai-api-key",
envVar: "OPENAI_API_KEY",
promptMessage: "Enter OpenAI API key",
defaultModel: "gpt-5.4",
expectedProviders: ["openai"],
applyConfig: (cfg) => applyOpenAIConfig(cfg),
wizard: {
title: "OpenAI Setup",
steps: [/* interactive setup steps */],
},
}),
],
createProviderApiKeyAuthMethod helper generates a full auth method from a declarative config. It handles env var lookup, CLI flag parsing, config file resolution, interactive prompts, and setup wizard rendering.
Model Resolution Pipeline
When a user requests a model like openai/gpt-5.4, the resolution pipeline proceeds through these stages:
flowchart TD Input["User requests model: openai/gpt-5.4"] Catalog["Check static catalog"] Dynamic["resolveDynamicModel()"] Normalize["normalizeResolvedModel()"] Compat["contributeResolvedModelCompat()"] Transport["normalizeTransport()"] Ready["ProviderRuntimeModel ready"] Input --> Catalog Catalog -->|Found| Normalize Catalog -->|Not found| Dynamic Dynamic -->|Resolved| Normalize Dynamic -->|null| Error["Unknown model error"] Normalize --> Compat Compat --> Transport Transport --> Ready
Stream Processing
The streaming pipeline supports two extension points: createStreamFn (replace the entire stream) and wrapStreamFn (wrap the default stream with middleware):
// From extensions/openai/openai-provider.ts
wrapStreamFn: (ctx) => {
return createOpenAIAttributionHeadersWrapper({
innerStream: ctx.defaultStreamFn,
modelId: ctx.model.id,
providerFamily: "openai",
});
},
wrapStreamFn is preferred over createStreamFn when you only need to add headers or logging. It preserves the default streaming implementation and avoids re-implementing SSE parsing.
Request Lifecycle
sequenceDiagram
participant Agent as Agent Engine
participant Resolver as Provider Resolver
participant Auth as Auth Chain
participant Provider as ProviderPlugin
participant API as Vendor API
Agent->>Resolver: resolveProvider("openai/gpt-5.4")
Resolver->>Provider: resolveDynamicModel()
Provider-->>Resolver: ProviderRuntimeModel
Resolver->>Auth: prepareRuntimeAuth()
Auth-->>Resolver: API key + headers
Resolver->>Provider: normalizeTransport()
Provider-->>Resolver: { baseUrl, api }
Resolver->>Provider: wrapStreamFn()
Provider-->>Resolver: Wrapped StreamFn
Agent->>API: POST /v1/chat/completions (streaming)
API-->>Agent: SSE token stream
The entire lifecycle from model selection to first token is typically under 100ms of local processing. Network latency to the vendor API dominates the total time-to-first-token.
Annotated Code Walkthrough
Walk through building a complete provider extension from scratch:
Define the provider metadata and auth chain. The id must be unique across all providers:
// extensions/my-provider/my-provider.ts
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/core";
import type { ProviderPlugin } from "openclaw/plugin-sdk/core";
const PROVIDER_ID = "my-provider";
export function buildMyProvider(): ProviderPlugin {
return {
id: PROVIDER_ID,
label: "My Provider",
docsPath: "/providers/my-provider",
envVars: ["MY_PROVIDER_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "My Provider API key",
envVar: "MY_PROVIDER_API_KEY",
flagName: "--my-provider-api-key",
defaultModel: "my-model-v1",
expectedProviders: [PROVIDER_ID],
applyConfig: (cfg) => ({
...cfg,
baseUrl: "https://api.myprovider.com/v1",
}),
}),
],
};
}
Add model catalog and dynamic resolution for forward compatibility with new models:
// Continue in my-provider.ts
export function buildMyProvider(): ProviderPlugin {
return {
// ... id, label, auth from Step 1 ...
catalog: {
models: [
{ id: "my-model-v1", label: "My Model v1", contextWindow: 128000 },
{ id: "my-model-v2", label: "My Model v2", contextWindow: 256000 },
],
},
resolveDynamicModel: (ctx) => {
// Accept any model prefixed with "my-model-"
if (ctx.modelId.startsWith("my-model-")) {
return {
id: ctx.modelId,
label: ctx.modelId,
provider: PROVIDER_ID,
};
}
return null;
},
normalizeTransport: ({ provider, api, baseUrl }) => ({
api: api ?? "openai", // Use OpenAI-compatible API format
baseUrl: baseUrl ?? "https://api.myprovider.com/v1",
}),
capabilities: { providerFamily: "openai" },
};
}
api: "openai" in normalizeTransport tells the engine to use the OpenAI-compatible chat completions format. Most providers support this, making integration trivial.
Register the provider in the extension entry point:
// extensions/my-provider/index.ts
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { buildMyProvider } from "./my-provider.js";
export default definePluginEntry({
id: "my-provider",
name: "My Provider",
description: "Custom LLM provider integration",
register(api) {
api.registerProvider(buildMyProvider());
},
});
Edge Cases & Error Handling
| Scenario | Behavior | Code Location |
|---|---|---|
| No auth method succeeds | buildMissingAuthMessage() generates a user-friendly error with setup instructions | openai-provider.ts |
| Unknown model ID requested | resolveDynamicModel() returns null; buildUnknownModelHint() suggests similar models | Provider resolver |
| Vendor API returns 429 (rate limit) | buildReplayPolicy() defines retry intervals with exponential backoff | Stream wrapper |
| Model deprecated by vendor | suppressBuiltInModel() hides it from catalog; augmentModelCatalog() adds replacement | Provider plugin |
| Transport normalization fails | Falls back to default base URL; warning logged | normalizeTransport() |
| Stream connection drops mid-response | Engine retries with partial context if buildReplayPolicy() permits | Agent engine |
Provider Capabilities Reference
| Capability | Type | Description |
|---|---|---|
providerFamily | string | API compatibility family (e.g., "openai", "anthropic") |
supportsXHighThinking | boolean | Whether the model supports extended reasoning/thinking tokens |
isModernModelRef | boolean | Whether the model ID uses the modern naming convention |
normalizeToolSchemas | function | Custom tool schema transformation for vendor-specific formats |
createEmbeddingProvider | function | Factory for vector embedding generation |
Configuration
| Config Key | Default | Description |
|---|---|---|
envVars | [] | Environment variables the provider reads for API keys |
auth[].optionKey | — | Config file key for the API key (e.g., openaiApiKey) |
auth[].flagName | — | CLI flag name (e.g., --openai-api-key) |
auth[].defaultModel | — | Model to use when user doesn’t specify one |
normalizeTransport.baseUrl | Vendor default | Override API base URL (for proxies, self-hosted endpoints) |
Related Topics
See also:
- Extensions overview — all extension categories and the full ecosystem
- Extension Contract — the shared registration interface all extensions use
- Channel Extensions — the parallel adapter pattern for messaging channels
Directory Map
Source files for provider adapters
extensions/
├── openai/
│ ├── index.ts ← Entry: registers provider, speech, image, media, CLI
│ ├── openai-provider.ts ← ProviderPlugin: auth, catalog, transport, streaming
│ ├── speech-provider.ts ← SpeechProviderPlugin: TTS via OpenAI
│ ├── image-generation-provider.ts ← ImageGenerationProviderPlugin: DALL-E
│ ├── media-understanding-provider.ts ← Media analysis (vision, audio)
│ └── cli-backend.ts ← CLI interactive backend (Codex)
├── anthropic/
│ ├── index.ts ← Entry: registers Anthropic provider
│ └── anthropic-provider.ts ← ProviderPlugin: Claude models, extended thinking
├── google/
│ ├── index.ts ← Entry: registers Google provider
│ └── google-provider.ts ← ProviderPlugin: Gemini models, multimodal
├── ollama/
│ ├── index.ts ← Entry: registers Ollama provider
│ └── ollama-provider.ts ← ProviderPlugin: local server, any GGUF model
├── groq/
│ ├── index.ts ← Entry: registers Groq provider
│ └── groq-provider.ts ← ProviderPlugin: ultra-fast inference
└── (30+ more provider extensions...)