Agents/Providers/ — Multi-Model Provider Adapters
Unified adapter layer for 12+ LLM providers with model fallback and auth management
The providers sub-module handles all communication between OpenClaw agents and external LLM APIs. It abstracts provider differences behind a common interface—normalizing payload formats, managing API key rotation, implementing cascading model fallback with error categorization, and performing provider-specific schema sanitization. Each provider adapter encapsulates authentication, request formatting, and response parsing so the rest of the agent system can remain provider-agnostic.
Multi-Provider Auth Rotation Model Fallback Error Categorization
📚 What is this & why does it exist?
The providers layer is the translation bridge between OpenClaw’s agent runtime and the diverse world of LLM APIs. Each provider (OpenAI, Anthropic, Google Gemini, AWS Bedrock, Moonshot, Minimax, OpenRouter, Z AI, and others) expects requests in a different format, authenticates differently, and returns responses with different structures. This module normalizes all of that into a single interface the rest of the agent system consumes.
💡 Real-world analogy
Think of the United Nations interpreters’ booth. Delegates from 12+ countries each speak a different language (provider API). The interpreters (provider adapters) sit between the delegates and the assembly floor (agent runtime), translating every statement into a common protocol in real time. When one interpreter is unavailable (API key cooldown), the booth manager (auth-profiles.ts) seamlessly switches to a backup interpreter. If a delegate’s microphone fails entirely (provider outage), the fallback system (model-fallback.ts) routes the conversation to a different delegate who can cover the same topic. The assembly never sees the complexity—they just hear a smooth, unified feed.
Contents
Architecture
flowchart TD
subgraph Resolution["Model Resolution"]
HOOK["resolveHookModelSelection()\nHook-based model override"]
RUNTIME["resolveEffectiveRuntimeModel()\nContext window calculation"]
REG["model.ts\nModel registry & lookup"]
end
subgraph Auth["Auth & Credential Layer"]
PROF["auth-profiles.ts\nProfile ordering & cooldown"]
HEALTH["auth-health.ts\nAuthProfileHealth status"]
SYNC["external CLI sync\nTTL-based refresh"]
end
subgraph Selection["Provider Selection"]
NORM["model.provider-normalization.ts\nProvider ID normalization"]
EXTRA["extra-params.ts\nProvider-specific parameters"]
CAPS["openrouter-model-capabilities.ts\nCapability flags"]
end
subgraph Adapters["Provider Adapters"]
OAI["OpenAI Adapter"]
ANT["Anthropic Adapter"]
GEM["Google/Gemini Adapter"]
BED["AWS Bedrock Adapter"]
MOON["Moonshot Adapter"]
MINI["Minimax Adapter"]
OR["OpenRouter Adapter"]
ZAI["Z AI Adapter"]
PROXY["Proxy Adapter"]
end
subgraph Sanitize["Schema Sanitization"]
CLEAN["cleanForGemini()\nJSON Schema cleanup"]
KEYWORDS["GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS"]
end
subgraph Fallback["Fallback & Recovery"]
FB["model-fallback.ts\nCascading fallback chain"]
ERR["FallbackSummaryError\nAll models exhausted"]
end
HOOK --> RUNTIME
RUNTIME --> REG
REG --> NORM
NORM --> Auth
Auth --> Selection
Selection --> Adapters
Adapters --> Sanitize
Adapters --> Fallback
Fallback -->|retry| Selection
Auth Profile State Machine
stateDiagram-v2 [*] --> Valid Valid --> Cooldown : API error received Cooldown --> ValidProbe : soonestCooldownExpiry passes ValidProbe --> Valid : Probe succeeds ValidProbe --> Cooldown : Probe fails Valid --> Expired : Credential expires Expired --> Valid : Manual refresh / CLI sync Cooldown --> Expired : Credential expires during cooldown note right of Cooldown : Tracks (now, expiry) per profile note right of Valid : AuthProfileHealth = ok
Key Classes & Functions
| Class / Function | File | Purpose |
|---|---|---|
FallbackSummaryError | model-fallback.ts | Error thrown when all models in the fallback chain are exhausted; aggregates per-model errors |
resolveHookModelSelection() | run/setup.ts | Resolves model selection through the hook system, allowing plugins to override model choice |
resolveEffectiveRuntimeModel() | run/setup.ts | Determines final runtime model with context window calculation and provider constraints |
resolveAuthProfileOrder() | auth-profiles.ts | Orders auth credentials by health status, preferring non-cooldown profiles first |
AuthProfileHealth | auth-health.ts | Health enum: ok | expiring | expired | missing | static |
cleanForGemini() | schema/clean-for-gemini.ts | Strips unsupported JSON Schema keywords from tool definitions before sending to Gemini |
GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS | schema/clean-for-gemini.ts | Set of JSON Schema keywords Gemini cannot process (e.g., $ref, allOf, oneOf) |
extra-params.ts | pi-embedded-runner/ | Provider-specific extra parameters appended to request payloads |
model.provider-normalization.ts | pi-embedded-runner/ | Normalizes provider ID strings across aliases and legacy names |
openrouter-model-capabilities.ts | pi-embedded-runner/ | Capability flags for OpenRouter-hosted models (tool use, vision, streaming) |
model.ts | pi-embedded-runner/ | Central model registry (~24KB); maps model IDs to provider, context window, and pricing |
soonestCooldownExpiry | auth-profiles.ts | Tracks per-profile cooldown expiry timestamps for automatic recovery scheduling |
AUTH_STORE_LOCK_OPTIONS | auth-profiles.ts | File lock config for concurrent auth store access (10 retries, 2× backoff) |
EXTERNAL_CLI_SYNC_TTL_MS | auth-profiles.ts | TTL (900 000 ms / 15 min) for external CLI credential synchronization |
Data Structures
| Type | Name | Description |
|---|---|---|
| enum | AuthProfileHealth | Health status: ok, expiring, expired, missing, static |
| type | ModelRegistryEntry | Registry record: provider, model ID, context window, max output tokens, pricing tier |
| type | FallbackChain | Ordered list of model candidates with per-model error categorization results |
| type | ProviderPayload | Normalized request payload after provider-specific formatting |
| type | AuthProfile | Credential record: API key, provider, cooldown state, expiry timestamp |
| const | EXTERNAL_CLI_SYNC_TTL_MS | 900 000 (15 min) — TTL for external CLI auth synchronization |
| const | AUTH_STORE_LOCK_OPTIONS | { retries: 10, factor: 2 } — File lock retry with exponential backoff |
| Set | GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS | JSON Schema keywords stripped before Gemini requests |
Interactions
| Direction | Partner | Interface | Description |
|---|---|---|---|
| → | LLM Provider APIs | HTTP / SSE | Sends formatted requests to provider endpoints via adapters |
| ← | Agent Runtime | run/setup.ts | Receives model selection requests and returns resolved provider config |
| → | Streaming Pipeline | Stream wrappers | Hands off authenticated, formatted requests to provider-specific stream wrappers |
| → | External CLI | File sync | Synchronizes credentials from external CLI tools on a 15-min TTL |
| ← | Hook System | resolveHookModelSelection() | Plugins can override model selection through hook callbacks |
| → | Schema Sanitizer | cleanForGemini() | Strips unsupported JSON Schema keywords before Gemini requests |
| → | Fallback Engine | model-fallback.ts | Cascades through alternative models on provider errors |
| ← | Config System | agents.list[].model | Reads primary model reference (provider/model) from agent configuration |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
agents.list[].model | — | Primary model ref in provider/model format (e.g., openai/gpt-4o) |
EXTERNAL_CLI_SYNC_TTL_MS | 900000 | External CLI auth synchronization TTL (15 minutes) |
AUTH_STORE_LOCK_OPTIONS.retries | 10 | File lock retry count for concurrent auth store access |
AUTH_STORE_LOCK_OPTIONS.factor | 2 | Exponential backoff multiplier for lock retries |
soonestCooldownExpiry | — | Per-profile cooldown expiry timestamp; auto-set on API errors |
model.contextWindow | varies | Context window size per model from the model registry |
model.maxOutputTokens | varies | Maximum output token count per model from the registry |
fallback.maxAttempts | 3 | Maximum number of fallback model attempts before throwing FallbackSummaryError |
fallback.errorCategories | — | Error classification: retryable, fatal, auth, rate-limit, context-overflow |
provider.streaming | true | Whether to use SSE streaming (some providers support non-streaming mode) |
gemini.schemaSanitization | true | Enable automatic stripping of unsupported JSON Schema keywords for Gemini |
openrouter.capabilities | — | Per-model capability flags: tool_use, vision, streaming, function_calling |
Error Categorization
The fallback engine classifies provider errors into categories that determine retry behavior:
| Category | HTTP Status | Behavior | Example |
|---|---|---|---|
retryable | 500, 502, 503 | Retry same model with backoff | Provider temporary outage |
rate_limit | 429 | Mark profile cooldown, try next key | API rate limit exceeded |
auth | 401, 403 | Mark profile expired, try next profile | Invalid or expired API key |
context_overflow | 400 | Cascade to model with larger context | Input exceeds context window |
content_filter | 400 | Do not retry; surface to user | Provider content policy violation |
fatal | * | Abort immediately | Malformed request structure |
Provider Capability Matrix
| Provider | Streaming | Tool Use | Vision | Thinking | Cache |
|---|---|---|---|---|---|
| OpenAI | ✓ | ✓ | ✓ | — | — |
| Anthropic | ✓ | ✓ | ✓ | ✓ | ✓ |
| Google / Gemini | ✓ | ✓ | ✓ | — | — |
| AWS Bedrock | ✓ | ✓ | ✓ | — | — |
| Moonshot | ✓ | ✓ | — | ✓ | — |
| Minimax | ✓ | ✓ | — | — | — |
| OpenRouter | ✓ | varies | varies | varies | — |
| Z AI | ✓ | ✓ | — | — | — |
| Proxy | ✓ | pass-through | pass-through | — | — |
Directory Map
Source tree for providers
pi-embedded-runner/
├── model.ts ← Central model registry (~24KB)
├── model.provider-normalization.ts ← Provider ID normalization
├── model-fallback.ts ← Cascading model fallback logic
├── auth-profiles.ts ← Auth profile ordering & cooldown
├── auth-health.ts ← AuthProfileHealth enum
├── extra-params.ts ← Provider-specific parameters
├── openrouter-model-capabilities.ts ← OpenRouter capability flags
├── schema/
│ └── clean-for-gemini.ts ← Gemini JSON Schema sanitizer
├── openai-stream-wrappers.ts ← OpenAI adapter
├── anthropic-cache-retention.ts ← Anthropic adapter
├── google-stream-wrappers.ts ← Google/Gemini adapter
├── bedrock-stream-wrappers.ts ← AWS Bedrock adapter
├── moonshot-stream-wrappers.ts ← Moonshot adapter
├── minimax-stream-wrappers.ts ← Minimax adapter
├── proxy-stream-wrappers.ts ← Generic proxy adapter
├── zai-stream-wrappers.ts ← Z AI adapter
└── run/
└── setup.ts ← Model resolution functions
Deep-Dive Highlights
Core provider mechanisms have been extracted into dedicated focus pages for in-depth exploration. Each page covers one aspect of the provider adapter layer with diagrams, code walks, and configuration details.
Deep-Dive Pages
Adapter Pattern Focus
Provider adapter architecture and stream wrapper design. How each provider’s API is normalized into a common interface with provider-specific payload formatting.
Token Management Focus
API key rotation, cooldown tracking, and auth profile ordering. How credentials are managed across providers with automatic recovery and external CLI sync.
Model Selection Focus
Model resolution, fallback chains, and runtime model switching. How the system picks the right model and recovers when providers fail.
Code Examples
Example: FallbackSummaryError Construction
// When all models in the fallback chain fail, FallbackSummaryError
// aggregates every per-model error into one structured report.
import { FallbackSummaryError } from './model-fallback';
const perModelErrors = [
{ model: 'openai/gpt-4o', error: 'rate_limit', status: 429 },
{ model: 'anthropic/claude-3.5', error: 'auth_expired', status: 401 },
{ model: 'google/gemini-pro', error: 'context_overflow', status: 400 },
];
throw new FallbackSummaryError(
`All ${perModelErrors.length} fallback models exhausted`,
perModelErrors, // per-model error details
'openai/gpt-4o', // original requested model
3, // total attempts
);
// FallbackSummaryError.perModelErrors gives structured access
// to each failure for logging and user-facing diagnostics.
Example: Auth Profile Ordering Logic
// resolveAuthProfileOrder() sorts credentials so the agent
// always tries the healthiest key first.
import { resolveAuthProfileOrder } from './auth-profiles';
const profiles = await resolveAuthProfileOrder('openai');
// Returns profiles sorted by:
// 1. AuthProfileHealth.ok — fully healthy, preferred
// 2. AuthProfileHealth.expiring — near expiry, usable
// 3. AuthProfileHealth.static — static key, no refresh
// 4. (skipped) expired / missing — excluded from rotation
//
// Each profile tracks `soonestCooldownExpiry` so the system
// knows when to re-probe a previously failed credential.
for (const profile of profiles) {
if (profile.cooldownExpiry && profile.cooldownExpiry > Date.now()) {
continue; // still in cooldown, skip
}
return profile; // use first available
}
// If none available: trigger EXTERNAL_CLI_SYNC_TTL_MS refresh
// (900_000 ms = 15 min TTL) then retry.
Example: Gemini Schema Sanitization
import { cleanForGemini, GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS } from './schema/clean-for-gemini';
// GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS includes:
// '$ref', 'allOf', 'oneOf', 'anyOf', 'not', 'if', 'then', 'else', ...
const toolSchema = {
type: 'object',
properties: {
query: { type: 'string', minLength: 1 },
filter: { oneOf: [{ type: 'string' }, { type: 'number' }] },
},
$ref: '#/definitions/SearchParams',
};
const cleaned = cleanForGemini(toolSchema);
// Result: { type: 'object', properties: { query: { type: 'string' }, filter: {} } }
// '$ref', 'oneOf', and 'minLength' stripped — Gemini cannot process them.