Provider Adapters

Focus Deep-Dive

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

Primary role

Translate vendor-specific LLM APIs into OpenClaw’s uniform provider contract for the agent engine.

Main inputs

User config (API keys, base URLs), model selection, chat messages, tool schemas.

Key decisions

Auth method selection, dynamic model resolution, transport normalization, thinking-level support.

Outputs / side effects

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

DecisionWhat controls itWhy it matters
Which auth method to useauth: ProviderAuthMethod[] array orderingFirst matching auth method wins; determines API key source and config flow
Dynamic model resolutionresolveDynamicModel(ctx)Allows forward-compatibility with unreleased models (e.g., gpt-5.4 before catalog update)
Transport overridenormalizeTransport(ctx)Redirects API calls through proxies, custom endpoints, or gateway overlays
Stream wrappingwrapStreamFn(ctx)Injects vendor-specific headers (attribution, tracing) without modifying the base stream
Thinking level supportsupportsXHighThinking(ctx)Enables extended reasoning for capable models (o-series, Claude 3.5+)
Model suppressionsuppressBuiltInModel(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 */],
    },
  }),
],
Key detail: The 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",
  });
},
Performance note: 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" },
  };
}
Compatibility tip: Setting 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

ScenarioBehaviorCode Location
No auth method succeedsbuildMissingAuthMessage() generates a user-friendly error with setup instructionsopenai-provider.ts
Unknown model ID requestedresolveDynamicModel() returns null; buildUnknownModelHint() suggests similar modelsProvider resolver
Vendor API returns 429 (rate limit)buildReplayPolicy() defines retry intervals with exponential backoffStream wrapper
Model deprecated by vendorsuppressBuiltInModel() hides it from catalog; augmentModelCatalog() adds replacementProvider plugin
Transport normalization failsFalls back to default base URL; warning loggednormalizeTransport()
Stream connection drops mid-responseEngine retries with partial context if buildReplayPolicy() permitsAgent engine

Provider Capabilities Reference

CapabilityTypeDescription
providerFamilystringAPI compatibility family (e.g., "openai", "anthropic")
supportsXHighThinkingbooleanWhether the model supports extended reasoning/thinking tokens
isModernModelRefbooleanWhether the model ID uses the modern naming convention
normalizeToolSchemasfunctionCustom tool schema transformation for vendor-specific formats
createEmbeddingProviderfunctionFactory for vector embedding generation

Configuration

Config KeyDefaultDescription
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.baseUrlVendor defaultOverride API base URL (for proxies, self-hosted endpoints)

See also:

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