mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:20:43 +00:00
Merge remote-tracking branch 'origin/main' into release/2026.4.25
This commit is contained in:
@@ -5,6 +5,7 @@ read_when:
|
||||
- Updating failover rules for auth profiles or models
|
||||
- Understanding how session model overrides interact with fallback retries
|
||||
title: "Model failover"
|
||||
sidebarTitle: "Model failover"
|
||||
---
|
||||
|
||||
OpenClaw handles failures in two stages:
|
||||
@@ -18,29 +19,31 @@ This doc explains the runtime rules and the data that backs them.
|
||||
|
||||
For a normal text run, OpenClaw evaluates candidates in this order:
|
||||
|
||||
1. The currently selected session model.
|
||||
2. Configured `agents.defaults.model.fallbacks` in order.
|
||||
3. The configured primary model at the end when the run started from an override.
|
||||
<Steps>
|
||||
<Step title="Resolve session state">
|
||||
Resolve the active session model and auth-profile preference.
|
||||
</Step>
|
||||
<Step title="Build candidate chain">
|
||||
Build the model candidate chain from the currently selected session model, then `agents.defaults.model.fallbacks` in order, ending with the configured primary when the run started from an override.
|
||||
</Step>
|
||||
<Step title="Try the current provider">
|
||||
Try the current provider with auth-profile rotation/cooldown rules.
|
||||
</Step>
|
||||
<Step title="Advance on failover-worthy errors">
|
||||
If that provider is exhausted with a failover-worthy error, move to the next model candidate.
|
||||
</Step>
|
||||
<Step title="Persist fallback override">
|
||||
Persist the selected fallback override before the retry starts so other session readers see the same provider/model the runner is about to use.
|
||||
</Step>
|
||||
<Step title="Roll back narrowly on failure">
|
||||
If the fallback candidate fails, roll back only the fallback-owned session override fields when they still match that failed candidate.
|
||||
</Step>
|
||||
<Step title="Throw FallbackSummaryError if exhausted">
|
||||
If every candidate fails, throw a `FallbackSummaryError` with per-attempt detail and the soonest cooldown expiry when one is known.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Inside each candidate, OpenClaw tries auth-profile failover before advancing to
|
||||
the next model candidate.
|
||||
|
||||
High-level sequence:
|
||||
|
||||
1. Resolve the active session model and auth-profile preference.
|
||||
2. Build the model candidate chain.
|
||||
3. Try the current provider with auth-profile rotation/cooldown rules.
|
||||
4. If that provider is exhausted with a failover-worthy error, move to the next
|
||||
model candidate.
|
||||
5. Persist the selected fallback override before the retry starts so other
|
||||
session readers see the same provider/model the runner is about to use.
|
||||
6. If the fallback candidate fails, roll back only the fallback-owned session
|
||||
override fields when they still match that failed candidate.
|
||||
7. If every candidate fails, throw a `FallbackSummaryError` with per-attempt
|
||||
detail and the soonest cooldown expiry when one is known.
|
||||
|
||||
This is intentionally narrower than "save and restore the whole session". The
|
||||
reply runner only persists the model-selection fields it owns for fallback:
|
||||
This is intentionally narrower than "save and restore the whole session". The reply runner only persists the model-selection fields it owns for fallback:
|
||||
|
||||
- `providerOverride`
|
||||
- `modelOverride`
|
||||
@@ -48,9 +51,7 @@ reply runner only persists the model-selection fields it owns for fallback:
|
||||
- `authProfileOverrideSource`
|
||||
- `authProfileOverrideCompactionCount`
|
||||
|
||||
That prevents a failed fallback retry from overwriting newer unrelated session
|
||||
mutations such as manual `/model` changes or session rotation updates that
|
||||
happened while the attempt was running.
|
||||
That prevents a failed fallback retry from overwriting newer unrelated session mutations such as manual `/model` changes or session rotation updates that happened while the attempt was running.
|
||||
|
||||
## Auth storage (keys + OAuth)
|
||||
|
||||
@@ -61,7 +62,7 @@ OpenClaw uses **auth profiles** for both API keys and OAuth tokens.
|
||||
- Config `auth.profiles` / `auth.order` are **metadata + routing only** (no secrets).
|
||||
- Legacy import-only OAuth file: `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use).
|
||||
|
||||
More detail: [/concepts/oauth](/concepts/oauth)
|
||||
More detail: [OAuth](/concepts/oauth)
|
||||
|
||||
Credential types:
|
||||
|
||||
@@ -81,9 +82,17 @@ Profiles live in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json` under `
|
||||
|
||||
When a provider has multiple profiles, OpenClaw chooses an order like this:
|
||||
|
||||
1. **Explicit config**: `auth.order[provider]` (if set).
|
||||
2. **Configured profiles**: `auth.profiles` filtered by provider.
|
||||
3. **Stored profiles**: entries in `auth-profiles.json` for the provider.
|
||||
<Steps>
|
||||
<Step title="Explicit config">
|
||||
`auth.order[provider]` (if set).
|
||||
</Step>
|
||||
<Step title="Configured profiles">
|
||||
`auth.profiles` filtered by provider.
|
||||
</Step>
|
||||
<Step title="Stored profiles">
|
||||
Entries in `auth-profiles.json` for the provider.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
If no explicit order is configured, OpenClaw uses a round‑robin order:
|
||||
|
||||
@@ -93,20 +102,17 @@ If no explicit order is configured, OpenClaw uses a round‑robin order:
|
||||
|
||||
### Session stickiness (cache-friendly)
|
||||
|
||||
OpenClaw **pins the chosen auth profile per session** to keep provider caches warm.
|
||||
It does **not** rotate on every request. The pinned profile is reused until:
|
||||
OpenClaw **pins the chosen auth profile per session** to keep provider caches warm. It does **not** rotate on every request. The pinned profile is reused until:
|
||||
|
||||
- the session is reset (`/new` / `/reset`)
|
||||
- a compaction completes (compaction count increments)
|
||||
- the profile is in cooldown/disabled
|
||||
|
||||
Manual selection via `/model …@<profileId>` sets a **user override** for that session
|
||||
and is not auto‑rotated until a new session starts.
|
||||
Manual selection via `/model …@<profileId>` sets a **user override** for that session and is not auto-rotated until a new session starts.
|
||||
|
||||
Auto‑pinned profiles (selected by the session router) are treated as a **preference**:
|
||||
they are tried first, but OpenClaw may rotate to another profile on rate limits/timeouts.
|
||||
User‑pinned profiles stay locked to that profile; if it fails and model fallbacks
|
||||
are configured, OpenClaw moves to the next model instead of switching profiles.
|
||||
<Note>
|
||||
Auto-pinned profiles (selected by the session router) are treated as a **preference**: they are tried first, but OpenClaw may rotate to another profile on rate limits/timeouts. User-pinned profiles stay locked to that profile; if it fails and model fallbacks are configured, OpenClaw moves to the next model instead of switching profiles.
|
||||
</Note>
|
||||
|
||||
### Why OAuth can "look lost"
|
||||
|
||||
@@ -117,45 +123,31 @@ If you have both an OAuth profile and an API key profile for the same provider,
|
||||
|
||||
## Cooldowns
|
||||
|
||||
When a profile fails due to auth/rate‑limit errors (or a timeout that looks
|
||||
like rate limiting), OpenClaw marks it in cooldown and moves to the next profile.
|
||||
That rate-limit bucket is broader than plain `429`: it also includes provider
|
||||
messages such as `Too many concurrent requests`, `ThrottlingException`,
|
||||
`concurrency limit reached`, `workers_ai ... quota limit exceeded`,
|
||||
`throttled`, `resource exhausted`, and periodic usage-window limits such as
|
||||
`weekly/monthly limit reached`.
|
||||
Format/invalid‑request errors (for example Cloud Code Assist tool call ID
|
||||
validation failures) are treated as failover‑worthy and use the same cooldowns.
|
||||
OpenAI-compatible stop-reason errors such as `Unhandled stop reason: error`,
|
||||
`stop reason: error`, and `reason: error` are classified as timeout/failover
|
||||
signals.
|
||||
Generic server text can also land in that timeout bucket when the source matches
|
||||
a known transient pattern. For example, the bare pi-ai stream-wrapper message
|
||||
`An unknown error occurred` is treated as failover-worthy for every provider
|
||||
because pi-ai emits it when provider streams end with `stopReason: "aborted"` or
|
||||
`stopReason: "error"` without specific details. JSON `api_error` payloads with
|
||||
transient server text such as `internal server error`, `unknown error, 520`,
|
||||
`upstream error`, or `backend error` are also treated as failover-worthy
|
||||
timeouts.
|
||||
OpenRouter-specific generic upstream text such as bare `Provider returned error`
|
||||
is treated as timeout only when the provider context is actually OpenRouter.
|
||||
Generic internal fallback text such as `LLM request failed with an unknown
|
||||
error.` stays conservative and does not trigger failover by itself.
|
||||
When a profile fails due to auth/rate-limit errors (or a timeout that looks like rate limiting), OpenClaw marks it in cooldown and moves to the next profile.
|
||||
|
||||
Some provider SDKs may otherwise sleep for a long `Retry-After` window before
|
||||
returning control to OpenClaw. For Stainless-based SDKs such as Anthropic and
|
||||
OpenAI, OpenClaw caps SDK-internal `retry-after-ms` / `retry-after` waits at 60
|
||||
seconds by default and surfaces longer retryable responses immediately so this
|
||||
failover path can run. Tune or disable the cap with
|
||||
`OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS`; see [/concepts/retry](/concepts/retry).
|
||||
<AccordionGroup>
|
||||
<Accordion title="What lands in the rate-limit / timeout bucket">
|
||||
That rate-limit bucket is broader than plain `429`: it also includes provider messages such as `Too many concurrent requests`, `ThrottlingException`, `concurrency limit reached`, `workers_ai ... quota limit exceeded`, `throttled`, `resource exhausted`, and periodic usage-window limits such as `weekly/monthly limit reached`.
|
||||
|
||||
Rate-limit cooldowns can also be model-scoped:
|
||||
Format/invalid-request errors (for example Cloud Code Assist tool call ID validation failures) are treated as failover-worthy and use the same cooldowns. OpenAI-compatible stop-reason errors such as `Unhandled stop reason: error`, `stop reason: error`, and `reason: error` are classified as timeout/failover signals.
|
||||
|
||||
- OpenClaw records `cooldownModel` for rate-limit failures when the failing
|
||||
model id is known.
|
||||
- A sibling model on the same provider can still be tried when the cooldown is
|
||||
scoped to a different model.
|
||||
- Billing/disabled windows still block the whole profile across models.
|
||||
Generic server text can also land in that timeout bucket when the source matches a known transient pattern. For example, the bare pi-ai stream-wrapper message `An unknown error occurred` is treated as failover-worthy for every provider because pi-ai emits it when provider streams end with `stopReason: "aborted"` or `stopReason: "error"` without specific details. JSON `api_error` payloads with transient server text such as `internal server error`, `unknown error, 520`, `upstream error`, or `backend error` are also treated as failover-worthy timeouts.
|
||||
|
||||
OpenRouter-specific generic upstream text such as bare `Provider returned error` is treated as timeout only when the provider context is actually OpenRouter. Generic internal fallback text such as `LLM request failed with an unknown error.` stays conservative and does not trigger failover by itself.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="SDK retry-after caps">
|
||||
Some provider SDKs may otherwise sleep for a long `Retry-After` window before returning control to OpenClaw. For Stainless-based SDKs such as Anthropic and OpenAI, OpenClaw caps SDK-internal `retry-after-ms` / `retry-after` waits at 60 seconds by default and surfaces longer retryable responses immediately so this failover path can run. Tune or disable the cap with `OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS`; see [Retry behavior](/concepts/retry).
|
||||
</Accordion>
|
||||
<Accordion title="Model-scoped cooldowns">
|
||||
Rate-limit cooldowns can also be model-scoped:
|
||||
|
||||
- OpenClaw records `cooldownModel` for rate-limit failures when the failing model id is known.
|
||||
- A sibling model on the same provider can still be tried when the cooldown is scoped to a different model.
|
||||
- Billing/disabled windows still block the whole profile across models.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Cooldowns use exponential backoff:
|
||||
|
||||
@@ -180,18 +172,13 @@ State is stored in `auth-state.json` under `usageStats`:
|
||||
|
||||
## Billing disables
|
||||
|
||||
Billing/credit failures (for example “insufficient credits” / “credit balance too low”) are treated as failover‑worthy, but they’re usually not transient. Instead of a short cooldown, OpenClaw marks the profile as **disabled** (with a longer backoff) and rotates to the next profile/provider.
|
||||
Billing/credit failures (for example "insufficient credits" / "credit balance too low") are treated as failover-worthy, but they're usually not transient. Instead of a short cooldown, OpenClaw marks the profile as **disabled** (with a longer backoff) and rotates to the next profile/provider.
|
||||
|
||||
Not every billing-shaped response is `402`, and not every HTTP `402` lands
|
||||
here. OpenClaw keeps explicit billing text in the billing lane even when a
|
||||
provider returns `401` or `403` instead, but provider-specific matchers stay
|
||||
scoped to the provider that owns them (for example OpenRouter `403 Key limit
|
||||
exceeded`). Meanwhile temporary `402` usage-window and
|
||||
organization/workspace spend-limit errors are classified as `rate_limit` when
|
||||
the message looks retryable (for example `weekly usage limit exhausted`, `daily
|
||||
limit reached, resets tomorrow`, or `organization spending limit exceeded`).
|
||||
Those stay on the short cooldown/failover path instead of the long
|
||||
billing-disable path.
|
||||
<Note>
|
||||
Not every billing-shaped response is `402`, and not every HTTP `402` lands here. OpenClaw keeps explicit billing text in the billing lane even when a provider returns `401` or `403` instead, but provider-specific matchers stay scoped to the provider that owns them (for example OpenRouter `403 Key limit exceeded`).
|
||||
|
||||
Meanwhile temporary `402` usage-window and organization/workspace spend-limit errors are classified as `rate_limit` when the message looks retryable (for example `weekly usage limit exhausted`, `daily limit reached, resets tomorrow`, or `organization spending limit exceeded`). Those stay on the short cooldown/failover path instead of the long billing-disable path.
|
||||
</Note>
|
||||
|
||||
State is stored in `auth-state.json`:
|
||||
|
||||
@@ -209,139 +196,114 @@ State is stored in `auth-state.json`:
|
||||
Defaults:
|
||||
|
||||
- Billing backoff starts at **5 hours**, doubles per billing failure, and caps at **24 hours**.
|
||||
- Backoff counters reset if the profile hasn’t failed for **24 hours** (configurable).
|
||||
- Backoff counters reset if the profile hasn't failed for **24 hours** (configurable).
|
||||
- Overloaded retries allow **1 same-provider profile rotation** before model fallback.
|
||||
- Overloaded retries use **0 ms backoff** by default.
|
||||
|
||||
## Model fallback
|
||||
|
||||
If all profiles for a provider fail, OpenClaw moves to the next model in
|
||||
`agents.defaults.model.fallbacks`. This applies to auth failures, rate limits, and
|
||||
timeouts that exhausted profile rotation (other errors do not advance fallback).
|
||||
If all profiles for a provider fail, OpenClaw moves to the next model in `agents.defaults.model.fallbacks`. This applies to auth failures, rate limits, and timeouts that exhausted profile rotation (other errors do not advance fallback).
|
||||
|
||||
Overloaded and rate-limit errors are handled more aggressively than billing
|
||||
cooldowns. By default, OpenClaw allows one same-provider auth-profile retry,
|
||||
then switches to the next configured model fallback without waiting.
|
||||
Provider-busy signals such as `ModelNotReadyException` land in that overloaded
|
||||
bucket. Tune this with `auth.cooldowns.overloadedProfileRotations`,
|
||||
`auth.cooldowns.overloadedBackoffMs`, and
|
||||
`auth.cooldowns.rateLimitedProfileRotations`.
|
||||
Overloaded and rate-limit errors are handled more aggressively than billing cooldowns. By default, OpenClaw allows one same-provider auth-profile retry, then switches to the next configured model fallback without waiting. Provider-busy signals such as `ModelNotReadyException` land in that overloaded bucket. Tune this with `auth.cooldowns.overloadedProfileRotations`, `auth.cooldowns.overloadedBackoffMs`, and `auth.cooldowns.rateLimitedProfileRotations`.
|
||||
|
||||
When a run starts with a model override (hooks or CLI), fallbacks still end at
|
||||
`agents.defaults.model.primary` after trying any configured fallbacks.
|
||||
When a run starts with a model override (hooks or CLI), fallbacks still end at `agents.defaults.model.primary` after trying any configured fallbacks.
|
||||
|
||||
### Candidate chain rules
|
||||
|
||||
OpenClaw builds the candidate list from the currently requested `provider/model`
|
||||
plus configured fallbacks.
|
||||
OpenClaw builds the candidate list from the currently requested `provider/model` plus configured fallbacks.
|
||||
|
||||
Rules:
|
||||
|
||||
- The requested model is always first.
|
||||
- Explicit configured fallbacks are deduplicated but not filtered by the model
|
||||
allowlist. They are treated as explicit operator intent.
|
||||
- If the current run is already on a configured fallback in the same provider
|
||||
family, OpenClaw keeps using the full configured chain.
|
||||
- If the current run is on a different provider than config and that current
|
||||
model is not already part of the configured fallback chain, OpenClaw does not
|
||||
append unrelated configured fallbacks from another provider.
|
||||
- When the run started from an override, the configured primary is appended at
|
||||
the end so the chain can settle back onto the normal default once earlier
|
||||
candidates are exhausted.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Rules">
|
||||
- The requested model is always first.
|
||||
- Explicit configured fallbacks are deduplicated but not filtered by the model allowlist. They are treated as explicit operator intent.
|
||||
- If the current run is already on a configured fallback in the same provider family, OpenClaw keeps using the full configured chain.
|
||||
- If the current run is on a different provider than config and that current model is not already part of the configured fallback chain, OpenClaw does not append unrelated configured fallbacks from another provider.
|
||||
- When the run started from an override, the configured primary is appended at the end so the chain can settle back onto the normal default once earlier candidates are exhausted.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Which errors advance fallback
|
||||
|
||||
Model fallback continues on:
|
||||
|
||||
- auth failures
|
||||
- rate limits and cooldown exhaustion
|
||||
- overloaded/provider-busy errors
|
||||
- timeout-shaped failover errors
|
||||
- billing disables
|
||||
- `LiveSessionModelSwitchError`, which is normalized into a failover path so a
|
||||
stale persisted model does not create an outer retry loop
|
||||
- other unrecognized errors when there are still remaining candidates
|
||||
|
||||
Model fallback does not continue on:
|
||||
|
||||
- explicit aborts that are not timeout/failover-shaped
|
||||
- context overflow errors that should stay inside compaction/retry logic
|
||||
(for example `request_too_large`, `INVALID_ARGUMENT: input exceeds the maximum
|
||||
number of tokens`, `input token count exceeds the maximum number of input
|
||||
tokens`, `The input is too long for the model`, or `ollama error: context
|
||||
length exceeded`)
|
||||
- a final unknown error when there are no candidates left
|
||||
<Tabs>
|
||||
<Tab title="Continues on">
|
||||
- auth failures
|
||||
- rate limits and cooldown exhaustion
|
||||
- overloaded/provider-busy errors
|
||||
- timeout-shaped failover errors
|
||||
- billing disables
|
||||
- `LiveSessionModelSwitchError`, which is normalized into a failover path so a stale persisted model does not create an outer retry loop
|
||||
- other unrecognized errors when there are still remaining candidates
|
||||
</Tab>
|
||||
<Tab title="Does not continue on">
|
||||
- explicit aborts that are not timeout/failover-shaped
|
||||
- context overflow errors that should stay inside compaction/retry logic (for example `request_too_large`, `INVALID_ARGUMENT: input exceeds the maximum number of tokens`, `input token count exceeds the maximum number of input tokens`, `The input is too long for the model`, or `ollama error: context length exceeded`)
|
||||
- a final unknown error when there are no candidates left
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Cooldown skip vs probe behavior
|
||||
|
||||
When every auth profile for a provider is already in cooldown, OpenClaw does
|
||||
not automatically skip that provider forever. It makes a per-candidate decision:
|
||||
When every auth profile for a provider is already in cooldown, OpenClaw does not automatically skip that provider forever. It makes a per-candidate decision:
|
||||
|
||||
- Persistent auth failures skip the whole provider immediately.
|
||||
- Billing disables usually skip, but the primary candidate can still be probed
|
||||
on a throttle so recovery is possible without restarting.
|
||||
- The primary candidate may be probed near cooldown expiry, with a per-provider
|
||||
throttle.
|
||||
- Same-provider fallback siblings can be attempted despite cooldown when the
|
||||
failure looks transient (`rate_limit`, `overloaded`, or unknown). This is
|
||||
especially relevant when a rate limit is model-scoped and a sibling model may
|
||||
still recover immediately.
|
||||
- Transient cooldown probes are limited to one per provider per fallback run so
|
||||
a single provider does not stall cross-provider fallback.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Per-candidate decisions">
|
||||
- Persistent auth failures skip the whole provider immediately.
|
||||
- Billing disables usually skip, but the primary candidate can still be probed on a throttle so recovery is possible without restarting.
|
||||
- The primary candidate may be probed near cooldown expiry, with a per-provider throttle.
|
||||
- Same-provider fallback siblings can be attempted despite cooldown when the failure looks transient (`rate_limit`, `overloaded`, or unknown). This is especially relevant when a rate limit is model-scoped and a sibling model may still recover immediately.
|
||||
- Transient cooldown probes are limited to one per provider per fallback run so a single provider does not stall cross-provider fallback.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Session overrides and live model switching
|
||||
|
||||
Session model changes are shared state. The active runner, `/model` command,
|
||||
compaction/session updates, and live-session reconciliation all read or write
|
||||
parts of the same session entry.
|
||||
Session model changes are shared state. The active runner, `/model` command, compaction/session updates, and live-session reconciliation all read or write parts of the same session entry.
|
||||
|
||||
That means fallback retries have to coordinate with live model switching:
|
||||
|
||||
- Only explicit user-driven model changes mark a pending live switch. That
|
||||
includes `/model`, `session_status(model=...)`, and `sessions.patch`.
|
||||
- System-driven model changes such as fallback rotation, heartbeat overrides,
|
||||
or compaction never mark a pending live switch on their own.
|
||||
- Before a fallback retry starts, the reply runner persists the selected
|
||||
fallback override fields to the session entry.
|
||||
- Live-session reconciliation prefers persisted session overrides over stale
|
||||
runtime model fields.
|
||||
- If the fallback attempt fails, the runner rolls back only the override fields
|
||||
it wrote, and only if they still match that failed candidate.
|
||||
- Only explicit user-driven model changes mark a pending live switch. That includes `/model`, `session_status(model=...)`, and `sessions.patch`.
|
||||
- System-driven model changes such as fallback rotation, heartbeat overrides, or compaction never mark a pending live switch on their own.
|
||||
- Before a fallback retry starts, the reply runner persists the selected fallback override fields to the session entry.
|
||||
- Live-session reconciliation prefers persisted session overrides over stale runtime model fields.
|
||||
- If the fallback attempt fails, the runner rolls back only the override fields it wrote, and only if they still match that failed candidate.
|
||||
|
||||
This prevents the classic race:
|
||||
|
||||
1. Primary fails.
|
||||
2. Fallback candidate is chosen in memory.
|
||||
3. Session store still says the old primary.
|
||||
4. Live-session reconciliation reads the stale session state.
|
||||
5. The retry gets snapped back to the old model before the fallback attempt
|
||||
starts.
|
||||
<Steps>
|
||||
<Step title="Primary fails">
|
||||
The selected primary model fails.
|
||||
</Step>
|
||||
<Step title="Fallback chosen in memory">
|
||||
Fallback candidate is chosen in memory.
|
||||
</Step>
|
||||
<Step title="Session store still says old primary">
|
||||
Session store still reflects the old primary.
|
||||
</Step>
|
||||
<Step title="Live reconciliation reads stale state">
|
||||
Live-session reconciliation reads the stale session state.
|
||||
</Step>
|
||||
<Step title="Retry snapped back">
|
||||
The retry gets snapped back to the old model before the fallback attempt starts.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The persisted fallback override closes that window, and the narrow rollback
|
||||
keeps newer manual or runtime session changes intact.
|
||||
The persisted fallback override closes that window, and the narrow rollback keeps newer manual or runtime session changes intact.
|
||||
|
||||
## Observability and failure summaries
|
||||
|
||||
`runWithModelFallback(...)` records per-attempt details that feed logs and
|
||||
user-facing cooldown messaging:
|
||||
`runWithModelFallback(...)` records per-attempt details that feed logs and user-facing cooldown messaging:
|
||||
|
||||
- provider/model attempted
|
||||
- reason (`rate_limit`, `overloaded`, `billing`, `auth`, `model_not_found`, and
|
||||
similar failover reasons)
|
||||
- reason (`rate_limit`, `overloaded`, `billing`, `auth`, `model_not_found`, and similar failover reasons)
|
||||
- optional status/code
|
||||
- human-readable error summary
|
||||
|
||||
When every candidate fails, OpenClaw throws `FallbackSummaryError`. The outer
|
||||
reply runner can use that to build a more specific message such as "all models
|
||||
are temporarily rate-limited" and include the soonest cooldown expiry when one
|
||||
is known.
|
||||
When every candidate fails, OpenClaw throws `FallbackSummaryError`. The outer reply runner can use that to build a more specific message such as "all models are temporarily rate-limited" and include the soonest cooldown expiry when one is known.
|
||||
|
||||
That cooldown summary is model-aware:
|
||||
|
||||
- unrelated model-scoped rate limits are ignored for the attempted
|
||||
provider/model chain
|
||||
- if the remaining block is a matching model-scoped rate limit, OpenClaw
|
||||
reports the last matching expiry that still blocks that model
|
||||
- unrelated model-scoped rate limits are ignored for the attempted provider/model chain
|
||||
- if the remaining block is a matching model-scoped rate limit, OpenClaw reports the last matching expiry that still blocks that model
|
||||
|
||||
## Related config
|
||||
|
||||
|
||||
@@ -5,37 +5,53 @@ read_when:
|
||||
- Changing model fallback behavior or selection UX
|
||||
- Updating model scan probes (tools/images)
|
||||
title: "Models CLI"
|
||||
sidebarTitle: "Models CLI"
|
||||
---
|
||||
|
||||
See [/concepts/model-failover](/concepts/model-failover) for auth profile
|
||||
rotation, cooldowns, and how that interacts with fallbacks.
|
||||
Quick provider overview + examples: [/concepts/model-providers](/concepts/model-providers).
|
||||
Model refs choose a provider and model. They do not usually choose the
|
||||
low-level agent runtime. For example, `openai/gpt-5.5` can run through the
|
||||
normal OpenAI provider path or through the Codex app-server runtime, depending
|
||||
on `agents.defaults.agentRuntime.id`. See
|
||||
[/concepts/agent-runtimes](/concepts/agent-runtimes).
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Model failover" href="/concepts/model-failover">
|
||||
Auth profile rotation, cooldowns, and how that interacts with fallbacks.
|
||||
</Card>
|
||||
<Card title="Model providers" href="/concepts/model-providers">
|
||||
Quick provider overview and examples.
|
||||
</Card>
|
||||
<Card title="Agent runtimes" href="/concepts/agent-runtimes">
|
||||
PI, Codex, and other agent loop runtimes.
|
||||
</Card>
|
||||
<Card title="Configuration reference" href="/gateway/config-agents#agent-defaults">
|
||||
Model config keys.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. For example, `openai/gpt-5.5` can run through the normal OpenAI provider path or through the Codex app-server runtime, depending on `agents.defaults.agentRuntime.id`. See [Agent runtimes](/concepts/agent-runtimes).
|
||||
|
||||
## How model selection works
|
||||
|
||||
OpenClaw selects models in this order:
|
||||
|
||||
1. **Primary** model (`agents.defaults.model.primary` or `agents.defaults.model`).
|
||||
2. **Fallbacks** in `agents.defaults.model.fallbacks` (in order).
|
||||
3. **Provider auth failover** happens inside a provider before moving to the
|
||||
next model.
|
||||
<Steps>
|
||||
<Step title="Primary model">
|
||||
`agents.defaults.model.primary` (or `agents.defaults.model`).
|
||||
</Step>
|
||||
<Step title="Fallbacks">
|
||||
`agents.defaults.model.fallbacks` (in order).
|
||||
</Step>
|
||||
<Step title="Provider auth failover">
|
||||
Auth failover happens inside a provider before moving to the next model.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Related:
|
||||
|
||||
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
|
||||
- `agents.defaults.imageModel` is used **only when** the primary model can’t accept images.
|
||||
- `agents.defaults.pdfModel` is used by the `pdf` tool. If omitted, the tool
|
||||
falls back to `agents.defaults.imageModel`, then the resolved session/default
|
||||
model.
|
||||
- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. If omitted, `image_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered image-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
|
||||
- `agents.defaults.musicGenerationModel` is used by the shared music-generation capability. If omitted, `music_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered music-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
|
||||
- `agents.defaults.videoGenerationModel` is used by the shared video-generation capability. If omitted, `video_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered video-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
|
||||
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)).
|
||||
<AccordionGroup>
|
||||
<Accordion title="Related model surfaces">
|
||||
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
|
||||
- `agents.defaults.imageModel` is used **only when** the primary model can't accept images.
|
||||
- `agents.defaults.pdfModel` is used by the `pdf` tool. If omitted, the tool falls back to `agents.defaults.imageModel`, then the resolved session/default model.
|
||||
- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. If omitted, `image_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered image-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
|
||||
- `agents.defaults.musicGenerationModel` is used by the shared music-generation capability. If omitted, `music_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered music-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
|
||||
- `agents.defaults.videoGenerationModel` is used by the shared video-generation capability. If omitted, `video_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered video-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
|
||||
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [Multi-agent routing](/concepts/multi-agent)).
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Quick model policy
|
||||
|
||||
@@ -45,14 +61,13 @@ Related:
|
||||
|
||||
## Onboarding (recommended)
|
||||
|
||||
If you don’t want to hand-edit config, run onboarding:
|
||||
If you don't want to hand-edit config, run onboarding:
|
||||
|
||||
```bash
|
||||
openclaw onboard
|
||||
```
|
||||
|
||||
It can set up model + auth for common providers, including **OpenAI Code (Codex)
|
||||
subscription** (OAuth) and **Anthropic** (API key or Claude CLI).
|
||||
It can set up model + auth for common providers, including **OpenAI Code (Codex) subscription** (OAuth) and **Anthropic** (API key or Claude CLI).
|
||||
|
||||
## Config keys (overview)
|
||||
|
||||
@@ -64,11 +79,11 @@ subscription** (OAuth) and **Anthropic** (API key or Claude CLI).
|
||||
- `agents.defaults.models` (allowlist + aliases + provider params)
|
||||
- `models.providers` (custom providers written into `models.json`)
|
||||
|
||||
Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize
|
||||
to `zai/*`.
|
||||
<Note>
|
||||
Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize to `zai/*`.
|
||||
|
||||
Provider configuration examples (including OpenCode) live in
|
||||
[/providers/opencode](/providers/opencode).
|
||||
Provider configuration examples (including OpenCode) live in [OpenCode](/providers/opencode).
|
||||
</Note>
|
||||
|
||||
### Safe allowlist edits
|
||||
|
||||
@@ -78,36 +93,30 @@ Use additive writes when updating `agents.defaults.models` by hand:
|
||||
openclaw config set agents.defaults.models '{"openai/gpt-5.4":{}}' --strict-json --merge
|
||||
```
|
||||
|
||||
`openclaw config set` protects model/provider maps from accidental clobbers. A
|
||||
plain object assignment to `agents.defaults.models`, `models.providers`, or
|
||||
`models.providers.<id>.models` is rejected when it would remove existing
|
||||
entries. Use `--merge` for additive changes; use `--replace` only when the
|
||||
provided value should become the complete target value.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Clobber protection rules">
|
||||
`openclaw config set` protects model/provider maps from accidental clobbers. A plain object assignment to `agents.defaults.models`, `models.providers`, or `models.providers.<id>.models` is rejected when it would remove existing entries. Use `--merge` for additive changes; use `--replace` only when the provided value should become the complete target value.
|
||||
|
||||
Interactive provider setup and `openclaw configure --section model` also merge
|
||||
provider-scoped selections into the existing allowlist, so adding Codex,
|
||||
Ollama, or another provider does not drop unrelated model entries.
|
||||
Configure preserves an existing `agents.defaults.model.primary` when provider
|
||||
auth is re-applied. Explicit default-setting commands such as
|
||||
`openclaw models auth login --provider <id> --set-default` and
|
||||
`openclaw models set <model>` still replace `agents.defaults.model.primary`.
|
||||
Interactive provider setup and `openclaw configure --section model` also merge provider-scoped selections into the existing allowlist, so adding Codex, Ollama, or another provider does not drop unrelated model entries. Configure preserves an existing `agents.defaults.model.primary` when provider auth is re-applied. Explicit default-setting commands such as `openclaw models auth login --provider <id> --set-default` and `openclaw models set <model>` still replace `agents.defaults.model.primary`.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## "Model is not allowed" (and why replies stop)
|
||||
|
||||
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for
|
||||
session overrides. When a user selects a model that isn’t in that allowlist,
|
||||
OpenClaw returns:
|
||||
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for session overrides. When a user selects a model that isn't in that allowlist, OpenClaw returns:
|
||||
|
||||
```
|
||||
Model "provider/model" is not allowed. Use /model to list available models.
|
||||
```
|
||||
|
||||
This happens **before** a normal reply is generated, so the message can feel
|
||||
like it “didn’t respond.” The fix is to either:
|
||||
<Warning>
|
||||
This happens **before** a normal reply is generated, so the message can feel like it "didn't respond." The fix is to either:
|
||||
|
||||
- Add the model to `agents.defaults.models`, or
|
||||
- Clear the allowlist (remove `agents.defaults.models`), or
|
||||
- Pick a model from `/model list`.
|
||||
</Warning>
|
||||
|
||||
Example allowlist config:
|
||||
|
||||
@@ -135,26 +144,29 @@ You can switch models for the current session without restarting:
|
||||
/model status
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `/model` (and `/model list`) is a compact, numbered picker (model family + available providers).
|
||||
- On Discord, `/model` and `/models` open an interactive picker with provider and model dropdowns plus a Submit step.
|
||||
- `/models add` is deprecated and now returns a deprecation message instead of registering models from chat.
|
||||
- `/model <#>` selects from that picker.
|
||||
- `/model` persists the new session selection immediately.
|
||||
- If the agent is idle, the next run uses the new model right away.
|
||||
- If a run is already active, OpenClaw marks a live switch as pending and only restarts into the new model at a clean retry point.
|
||||
- If tool activity or reply output has already started, the pending switch can stay queued until a later retry opportunity or the next user turn.
|
||||
- `/model status` is the detailed view (auth candidates and, when configured, provider endpoint `baseUrl` + `api` mode).
|
||||
- Model refs are parsed by splitting on the **first** `/`. Use `provider/model` when typing `/model <ref>`.
|
||||
- If the model ID itself contains `/` (OpenRouter-style), you must include the provider prefix (example: `/model openrouter/moonshotai/kimi-k2`).
|
||||
- If you omit the provider, OpenClaw resolves the input in this order:
|
||||
1. alias match
|
||||
2. unique configured-provider match for that exact unprefixed model id
|
||||
3. deprecated fallback to the configured default provider
|
||||
If that provider no longer exposes the configured default model, OpenClaw
|
||||
instead falls back to the first configured provider/model to avoid
|
||||
surfacing a stale removed-provider default.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Picker behavior">
|
||||
- `/model` (and `/model list`) is a compact, numbered picker (model family + available providers).
|
||||
- On Discord, `/model` and `/models` open an interactive picker with provider and model dropdowns plus a Submit step.
|
||||
- `/models add` is deprecated and now returns a deprecation message instead of registering models from chat.
|
||||
- `/model <#>` selects from that picker.
|
||||
</Accordion>
|
||||
<Accordion title="Persistence and live switching">
|
||||
- `/model` persists the new session selection immediately.
|
||||
- If the agent is idle, the next run uses the new model right away.
|
||||
- If a run is already active, OpenClaw marks a live switch as pending and only restarts into the new model at a clean retry point.
|
||||
- If tool activity or reply output has already started, the pending switch can stay queued until a later retry opportunity or the next user turn.
|
||||
- `/model status` is the detailed view (auth candidates and, when configured, provider endpoint `baseUrl` + `api` mode).
|
||||
</Accordion>
|
||||
<Accordion title="Ref parsing">
|
||||
- Model refs are parsed by splitting on the **first** `/`. Use `provider/model` when typing `/model <ref>`.
|
||||
- If the model ID itself contains `/` (OpenRouter-style), you must include the provider prefix (example: `/model openrouter/moonshotai/kimi-k2`).
|
||||
- If you omit the provider, OpenClaw resolves the input in this order:
|
||||
1. alias match
|
||||
2. unique configured-provider match for that exact unprefixed model id
|
||||
3. deprecated fallback to the configured default provider — if that provider no longer exposes the configured default model, OpenClaw instead falls back to the first configured provider/model to avoid surfacing a stale removed-provider default.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Full command behavior/config: [Slash commands](/tools/slash-commands).
|
||||
|
||||
@@ -187,38 +199,39 @@ openclaw models image-fallbacks clear
|
||||
|
||||
Shows configured models by default. Useful flags:
|
||||
|
||||
- `--all`: full catalog
|
||||
- `--local`: local providers only
|
||||
- `--provider <id>`: filter by provider id, for example `moonshot`; display
|
||||
labels from interactive pickers are not accepted
|
||||
- `--plain`: one model per line
|
||||
- `--json`: machine‑readable output
|
||||
|
||||
`--all` includes bundled provider-owned static catalog rows before auth is
|
||||
configured, so discovery-only views can show models that are unavailable until
|
||||
you add matching provider credentials.
|
||||
<ParamField path="--all" type="boolean">
|
||||
Full catalog. Includes bundled provider-owned static catalog rows before auth is configured, so discovery-only views can show models that are unavailable until you add matching provider credentials.
|
||||
</ParamField>
|
||||
<ParamField path="--local" type="boolean">
|
||||
Local providers only.
|
||||
</ParamField>
|
||||
<ParamField path="--provider <id>" type="string">
|
||||
Filter by provider id, for example `moonshot`. Display labels from interactive pickers are not accepted.
|
||||
</ParamField>
|
||||
<ParamField path="--plain" type="boolean">
|
||||
One model per line.
|
||||
</ParamField>
|
||||
<ParamField path="--json" type="boolean">
|
||||
Machine-readable output.
|
||||
</ParamField>
|
||||
|
||||
### `models status`
|
||||
|
||||
Shows the resolved primary model, fallbacks, image model, and an auth overview
|
||||
of configured providers. It also surfaces OAuth expiry status for profiles found
|
||||
in the auth store (warns within 24h by default). `--plain` prints only the
|
||||
resolved primary model.
|
||||
OAuth status is always shown (and included in `--json` output). If a configured
|
||||
provider has no credentials, `models status` prints a **Missing auth** section.
|
||||
JSON includes `auth.oauth` (warn window + profiles) and `auth.providers`
|
||||
(effective auth per provider, including env-backed credentials). `auth.oauth`
|
||||
is auth-store profile health only; env-only providers do not appear there.
|
||||
Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
|
||||
Use `--probe` for live auth checks; probe rows can come from auth profiles, env
|
||||
credentials, or `models.json`.
|
||||
If explicit `auth.order.<provider>` omits a stored profile, probe reports
|
||||
`excluded_by_auth_order` instead of trying it. If auth exists but no probeable
|
||||
model can be resolved for that provider, probe reports `status: no_model`.
|
||||
Shows the resolved primary model, fallbacks, image model, and an auth overview of configured providers. It also surfaces OAuth expiry status for profiles found in the auth store (warns within 24h by default). `--plain` prints only the resolved primary model.
|
||||
|
||||
Auth choice is provider/account dependent. For always-on gateway hosts, API
|
||||
keys are usually the most predictable; Claude CLI reuse and existing Anthropic
|
||||
OAuth/token profiles are also supported.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Auth and probe behavior">
|
||||
- OAuth status is always shown (and included in `--json` output). If a configured provider has no credentials, `models status` prints a **Missing auth** section.
|
||||
- JSON includes `auth.oauth` (warn window + profiles) and `auth.providers` (effective auth per provider, including env-backed credentials). `auth.oauth` is auth-store profile health only; env-only providers do not appear there.
|
||||
- Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
|
||||
- Use `--probe` for live auth checks; probe rows can come from auth profiles, env credentials, or `models.json`.
|
||||
- If explicit `auth.order.<provider>` omits a stored profile, probe reports `excluded_by_auth_order` instead of trying it. If auth exists but no probeable model can be resolved for that provider, probe reports `status: no_model`.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
Auth choice is provider/account dependent. For always-on gateway hosts, API keys are usually the most predictable; Claude CLI reuse and existing Anthropic OAuth/token profiles are also supported.
|
||||
</Note>
|
||||
|
||||
Example (Claude CLI):
|
||||
|
||||
@@ -229,24 +242,33 @@ openclaw models status
|
||||
|
||||
## Scanning (OpenRouter free models)
|
||||
|
||||
`openclaw models scan` inspects OpenRouter’s **free model catalog** and can
|
||||
optionally probe models for tool and image support.
|
||||
`openclaw models scan` inspects OpenRouter's **free model catalog** and can optionally probe models for tool and image support.
|
||||
|
||||
Key flags:
|
||||
<ParamField path="--no-probe" type="boolean">
|
||||
Skip live probes (metadata only).
|
||||
</ParamField>
|
||||
<ParamField path="--min-params <b>" type="number">
|
||||
Minimum parameter size (billions).
|
||||
</ParamField>
|
||||
<ParamField path="--max-age-days <days>" type="number">
|
||||
Skip older models.
|
||||
</ParamField>
|
||||
<ParamField path="--provider <name>" type="string">
|
||||
Provider prefix filter.
|
||||
</ParamField>
|
||||
<ParamField path="--max-candidates <n>" type="number">
|
||||
Fallback list size.
|
||||
</ParamField>
|
||||
<ParamField path="--set-default" type="boolean">
|
||||
Set `agents.defaults.model.primary` to the first selection.
|
||||
</ParamField>
|
||||
<ParamField path="--set-image" type="boolean">
|
||||
Set `agents.defaults.imageModel.primary` to the first image selection.
|
||||
</ParamField>
|
||||
|
||||
- `--no-probe`: skip live probes (metadata only)
|
||||
- `--min-params <b>`: minimum parameter size (billions)
|
||||
- `--max-age-days <days>`: skip older models
|
||||
- `--provider <name>`: provider prefix filter
|
||||
- `--max-candidates <n>`: fallback list size
|
||||
- `--set-default`: set `agents.defaults.model.primary` to the first selection
|
||||
- `--set-image`: set `agents.defaults.imageModel.primary` to the first image selection
|
||||
|
||||
The OpenRouter `/models` catalog is public, so metadata-only scans can list
|
||||
free candidates without a key. Probing and inference still require an
|
||||
OpenRouter API key (from auth profiles or `OPENROUTER_API_KEY`). If no key is
|
||||
available, `openclaw models scan` falls back to metadata-only output and leaves
|
||||
config unchanged. Use `--no-probe` to request metadata-only mode explicitly.
|
||||
<Note>
|
||||
The OpenRouter `/models` catalog is public, so metadata-only scans can list free candidates without a key. Probing and inference still require an OpenRouter API key (from auth profiles or `OPENROUTER_API_KEY`). If no key is available, `openclaw models scan` falls back to metadata-only output and leaves config unchanged. Use `--no-probe` to request metadata-only mode explicitly.
|
||||
</Note>
|
||||
|
||||
Scan results are ranked by:
|
||||
|
||||
@@ -255,42 +277,43 @@ Scan results are ranked by:
|
||||
3. Context size
|
||||
4. Parameter count
|
||||
|
||||
Input
|
||||
Input:
|
||||
|
||||
- OpenRouter `/models` list (filter `:free`)
|
||||
- Live probes require OpenRouter API key from auth profiles or `OPENROUTER_API_KEY` (see [/environment](/help/environment))
|
||||
- Live probes require OpenRouter API key from auth profiles or `OPENROUTER_API_KEY` (see [Environment variables](/help/environment))
|
||||
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`
|
||||
- Request/probe controls: `--timeout`, `--concurrency`
|
||||
|
||||
When live probes run in a TTY, you can select fallbacks interactively. In
|
||||
non‑interactive mode, pass `--yes` to accept defaults. Metadata-only results are
|
||||
informational; `--set-default` and `--set-image` require live probes so
|
||||
OpenClaw does not configure an unusable keyless OpenRouter model.
|
||||
When live probes run in a TTY, you can select fallbacks interactively. In non-interactive mode, pass `--yes` to accept defaults. Metadata-only results are informational; `--set-default` and `--set-image` require live probes so OpenClaw does not configure an unusable keyless OpenRouter model.
|
||||
|
||||
## Models registry (`models.json`)
|
||||
|
||||
Custom providers in `models.providers` are written into `models.json` under the
|
||||
agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). This file
|
||||
is merged by default unless `models.mode` is set to `replace`.
|
||||
Custom providers in `models.providers` are written into `models.json` under the agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). This file is merged by default unless `models.mode` is set to `replace`.
|
||||
|
||||
Merge mode precedence for matching provider IDs:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Merge mode precedence">
|
||||
Merge mode precedence for matching provider IDs:
|
||||
|
||||
- Non-empty `baseUrl` already present in the agent `models.json` wins.
|
||||
- Non-empty `apiKey` in the agent `models.json` wins only when that provider is not SecretRef-managed in current config/auth-profile context.
|
||||
- SecretRef-managed provider `apiKey` values are refreshed from source markers (`ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs) instead of persisting resolved secrets.
|
||||
- SecretRef-managed provider header values are refreshed from source markers (`secretref-env:ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs).
|
||||
- Empty or missing agent `apiKey`/`baseUrl` fall back to config `models.providers`.
|
||||
- Other provider fields are refreshed from config and normalized catalog data.
|
||||
- Non-empty `baseUrl` already present in the agent `models.json` wins.
|
||||
- Non-empty `apiKey` in the agent `models.json` wins only when that provider is not SecretRef-managed in current config/auth-profile context.
|
||||
- SecretRef-managed provider `apiKey` values are refreshed from source markers (`ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs) instead of persisting resolved secrets.
|
||||
- SecretRef-managed provider header values are refreshed from source markers (`secretref-env:ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs).
|
||||
- Empty or missing agent `apiKey`/`baseUrl` fall back to config `models.providers`.
|
||||
- Other provider fields are refreshed from config and normalized catalog data.
|
||||
|
||||
Marker persistence is source-authoritative: OpenClaw writes markers from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
|
||||
This applies whenever OpenClaw regenerates `models.json`, including command-driven paths like `openclaw agent`.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
Marker persistence is source-authoritative: OpenClaw writes markers from the active source config snapshot (pre-resolution), not from resolved runtime secret values. This applies whenever OpenClaw regenerates `models.json`, including command-driven paths like `openclaw agent`.
|
||||
</Note>
|
||||
|
||||
## Related
|
||||
|
||||
- [Model Providers](/concepts/model-providers) — provider routing and auth
|
||||
- [Agent Runtimes](/concepts/agent-runtimes) — PI, Codex, and other agent loop runtimes
|
||||
- [Model Failover](/concepts/model-failover) — fallback chains
|
||||
- [Image Generation](/tools/image-generation) — image model configuration
|
||||
- [Music Generation](/tools/music-generation) — music model configuration
|
||||
- [Video Generation](/tools/video-generation) — video model configuration
|
||||
- [Configuration Reference](/gateway/config-agents#agent-defaults) — model config keys
|
||||
- [Agent runtimes](/concepts/agent-runtimes) — PI, Codex, and other agent loop runtimes
|
||||
- [Configuration reference](/gateway/config-agents#agent-defaults) — model config keys
|
||||
- [Image generation](/tools/image-generation) — image model configuration
|
||||
- [Model failover](/concepts/model-failover) — fallback chains
|
||||
- [Model providers](/concepts/model-providers) — provider routing and auth
|
||||
- [Music generation](/tools/music-generation) — music model configuration
|
||||
- [Video generation](/tools/video-generation) — video model configuration
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
summary: "Multi-agent routing: isolated agents, channel accounts, and bindings"
|
||||
title: Multi-agent routing
|
||||
title: "Multi-agent routing"
|
||||
sidebarTitle: "Multi-agent routing"
|
||||
read_when: "You want multiple isolated agents (workspaces + auth) in one gateway process."
|
||||
status: active
|
||||
---
|
||||
@@ -23,32 +24,21 @@ Auth profiles are **per-agent**. Each agent reads from its own:
|
||||
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
|
||||
```
|
||||
|
||||
`sessions_history` is the safer cross-session recall path here too: it returns
|
||||
a bounded, sanitized view, not a raw transcript dump. Assistant recall strips
|
||||
thinking tags, `<relevant-memories>` scaffolding, plain-text tool-call XML
|
||||
payloads (including `<tool_call>...</tool_call>`,
|
||||
`<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`,
|
||||
`<function_calls>...</function_calls>`, and truncated tool-call blocks),
|
||||
downgraded tool-call scaffolding, leaked ASCII/full-width model control
|
||||
tokens, and malformed MiniMax tool-call XML before redaction/truncation.
|
||||
<Note>
|
||||
`sessions_history` is the safer cross-session recall path here too: it returns a bounded, sanitized view, not a raw transcript dump. Assistant recall strips thinking tags, `<relevant-memories>` scaffolding, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks), downgraded tool-call scaffolding, leaked ASCII/full-width model control tokens, and malformed MiniMax tool-call XML before redaction/truncation.
|
||||
</Note>
|
||||
|
||||
Main agent credentials are **not** shared automatically. Never reuse `agentDir`
|
||||
across agents (it causes auth/session collisions). If you want to share creds,
|
||||
copy `auth-profiles.json` into the other agent's `agentDir`.
|
||||
<Warning>
|
||||
Main agent credentials are **not** shared automatically. Never reuse `agentDir` across agents (it causes auth/session collisions). If you want to share creds, copy `auth-profiles.json` into the other agent's `agentDir`.
|
||||
</Warning>
|
||||
|
||||
Skills are loaded from each agent workspace plus shared roots such as
|
||||
`~/.openclaw/skills`, then filtered by the effective agent skill allowlist when
|
||||
configured. Use `agents.defaults.skills` for a shared baseline and
|
||||
`agents.list[].skills` for per-agent replacement. See
|
||||
[Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and
|
||||
[Skills: agent skill allowlists](/tools/skills#agent-skill-allowlists).
|
||||
Skills are loaded from each agent workspace plus shared roots such as `~/.openclaw/skills`, then filtered by the effective agent skill allowlist when configured. Use `agents.defaults.skills` for a shared baseline and `agents.list[].skills` for per-agent replacement. See [Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and [Skills: agent skill allowlists](/tools/skills#agent-skill-allowlists).
|
||||
|
||||
The Gateway can host **one agent** (default) or **many agents** side-by-side.
|
||||
|
||||
**Workspace note:** each agent’s workspace is the **default cwd**, not a hard
|
||||
sandbox. Relative paths resolve inside the workspace, but absolute paths can
|
||||
reach other host locations unless sandboxing is enabled. See
|
||||
[Sandboxing](/gateway/sandboxing).
|
||||
<Note>
|
||||
**Workspace note:** each agent's workspace is the **default cwd**, not a hard sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See [Sandboxing](/gateway/sandboxing).
|
||||
</Note>
|
||||
|
||||
## Paths (quick map)
|
||||
|
||||
@@ -87,48 +77,39 @@ openclaw agents list --bindings
|
||||
|
||||
<Steps>
|
||||
<Step title="Create each agent workspace">
|
||||
Use the wizard or create workspaces manually:
|
||||
|
||||
Use the wizard or create workspaces manually:
|
||||
```bash
|
||||
openclaw agents add coding
|
||||
openclaw agents add social
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw agents add coding
|
||||
openclaw agents add social
|
||||
```
|
||||
|
||||
Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/<agentId>`.
|
||||
Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/<agentId>`.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Create channel accounts">
|
||||
Create one account per agent on your preferred channels:
|
||||
|
||||
Create one account per agent on your preferred channels:
|
||||
- Discord: one bot per agent, enable Message Content Intent, copy each token.
|
||||
- Telegram: one bot per agent via BotFather, copy each token.
|
||||
- WhatsApp: link each phone number per account.
|
||||
|
||||
- Discord: one bot per agent, enable Message Content Intent, copy each token.
|
||||
- Telegram: one bot per agent via BotFather, copy each token.
|
||||
- WhatsApp: link each phone number per account.
|
||||
```bash
|
||||
openclaw channels login --channel whatsapp --account work
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw channels login --channel whatsapp --account work
|
||||
```
|
||||
|
||||
See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp).
|
||||
See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add agents, accounts, and bindings">
|
||||
|
||||
Add agents under `agents.list`, channel accounts under `channels.<channel>.accounts`, and connect them with `bindings` (examples below).
|
||||
|
||||
Add agents under `agents.list`, channel accounts under `channels.<channel>.accounts`, and connect them with `bindings` (examples below).
|
||||
</Step>
|
||||
|
||||
<Step title="Restart and verify">
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
openclaw agents list --bindings
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
openclaw agents list --bindings
|
||||
openclaw channels status --probe
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -140,14 +121,11 @@ With **multiple agents**, each `agentId` becomes a **fully isolated persona**:
|
||||
- **Different personalities** (per-agent workspace files like `AGENTS.md` and `SOUL.md`).
|
||||
- **Separate auth + sessions** (no cross-talk unless explicitly enabled).
|
||||
|
||||
This lets **multiple people** share one Gateway server while keeping their AI “brains” and data isolated.
|
||||
This lets **multiple people** share one Gateway server while keeping their AI "brains" and data isolated.
|
||||
|
||||
## Cross-agent QMD memory search
|
||||
|
||||
If one agent should search another agent's QMD session transcripts, add
|
||||
extra collections under `agents.list[].memorySearch.qmd.extraCollections`.
|
||||
Use `agents.defaults.memorySearch.qmd.extraCollections` only when every agent
|
||||
should inherit the same shared transcript collections.
|
||||
If one agent should search another agent's QMD session transcripts, add extra collections under `agents.list[].memorySearch.qmd.extraCollections`. Use `agents.defaults.memorySearch.qmd.extraCollections` only when every agent should inherit the same shared transcript collections.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -180,15 +158,15 @@ should inherit the same shared transcript collections.
|
||||
}
|
||||
```
|
||||
|
||||
The extra collection path can be shared across agents, but the collection name
|
||||
stays explicit when the path is outside the agent workspace. Paths inside the
|
||||
workspace remain agent-scoped so each agent keeps its own transcript search set.
|
||||
The extra collection path can be shared across agents, but the collection name stays explicit when the path is outside the agent workspace. Paths inside the workspace remain agent-scoped so each agent keeps its own transcript search set.
|
||||
|
||||
## One WhatsApp number, multiple people (DM split)
|
||||
|
||||
You can route **different WhatsApp DMs** to different agents while staying on **one WhatsApp account**. Match on sender E.164 (like `+15551234567`) with `peer.kind: "direct"`. Replies still come from the same WhatsApp number (no per‑agent sender identity).
|
||||
You can route **different WhatsApp DMs** to different agents while staying on **one WhatsApp account**. Match on sender E.164 (like `+15551234567`) with `peer.kind: "direct"`. Replies still come from the same WhatsApp number (no per-agent sender identity).
|
||||
|
||||
Important detail: direct chats collapse to the agent’s **main session key**, so true isolation requires **one agent per person**.
|
||||
<Note>
|
||||
Direct chats collapse to the agent's **main session key**, so true isolation requires **one agent per person**.
|
||||
</Note>
|
||||
|
||||
Example:
|
||||
|
||||
@@ -228,33 +206,50 @@ Notes:
|
||||
|
||||
Bindings are **deterministic** and **most-specific wins**:
|
||||
|
||||
1. `peer` match (exact DM/group/channel id)
|
||||
2. `parentPeer` match (thread inheritance)
|
||||
3. `guildId + roles` (Discord role routing)
|
||||
4. `guildId` (Discord)
|
||||
5. `teamId` (Slack)
|
||||
6. `accountId` match for a channel
|
||||
7. channel-level match (`accountId: "*"`)
|
||||
8. fallback to default agent (`agents.list[].default`, else first list entry, default: `main`)
|
||||
<Steps>
|
||||
<Step title="peer match">
|
||||
Exact DM/group/channel id.
|
||||
</Step>
|
||||
<Step title="parentPeer match">
|
||||
Thread inheritance.
|
||||
</Step>
|
||||
<Step title="guildId + roles">
|
||||
Discord role routing.
|
||||
</Step>
|
||||
<Step title="guildId">
|
||||
Discord.
|
||||
</Step>
|
||||
<Step title="teamId">
|
||||
Slack.
|
||||
</Step>
|
||||
<Step title="accountId match for a channel">
|
||||
Per-account fallback.
|
||||
</Step>
|
||||
<Step title="Channel-level match">
|
||||
`accountId: "*"`.
|
||||
</Step>
|
||||
<Step title="Default agent">
|
||||
Fallback to `agents.list[].default`, else first list entry, default: `main`.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
If multiple bindings match in the same tier, the first one in config order wins.
|
||||
If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics).
|
||||
|
||||
Important account-scope detail:
|
||||
|
||||
- A binding that omits `accountId` matches the default account only.
|
||||
- Use `accountId: "*"` for a channel-wide fallback across all accounts.
|
||||
- If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Tie-breaking and AND semantics">
|
||||
- If multiple bindings match in the same tier, the first one in config order wins.
|
||||
- If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics).
|
||||
</Accordion>
|
||||
<Accordion title="Account-scope detail">
|
||||
- A binding that omits `accountId` matches the default account only.
|
||||
- Use `accountId: "*"` for a channel-wide fallback across all accounts.
|
||||
- If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Multiple accounts / phone numbers
|
||||
|
||||
Channels that support **multiple accounts** (e.g. WhatsApp) use `accountId` to identify
|
||||
each login. Each `accountId` can be routed to a different agent, so one server can host
|
||||
multiple phone numbers without mixing sessions.
|
||||
Channels that support **multiple accounts** (e.g. WhatsApp) use `accountId` to identify each login. Each `accountId` can be routed to a different agent, so one server can host multiple phone numbers without mixing sessions.
|
||||
|
||||
If you want a channel-wide default account when `accountId` is omitted, set
|
||||
`channels.<channel>.defaultAccount` (optional). When unset, OpenClaw falls back
|
||||
to `default` if present, otherwise the first configured account id (sorted).
|
||||
If you want a channel-wide default account when `accountId` is omitted, set `channels.<channel>.defaultAccount` (optional). When unset, OpenClaw falls back to `default` if present, otherwise the first configured account id (sorted).
|
||||
|
||||
Common channels supporting this pattern include:
|
||||
|
||||
@@ -264,297 +259,298 @@ Common channels supporting this pattern include:
|
||||
|
||||
## Concepts
|
||||
|
||||
- `agentId`: one “brain” (workspace, per-agent auth, per-agent session store).
|
||||
- `agentId`: one "brain" (workspace, per-agent auth, per-agent session store).
|
||||
- `accountId`: one channel account instance (e.g. WhatsApp account `"personal"` vs `"biz"`).
|
||||
- `binding`: routes inbound messages to an `agentId` by `(channel, accountId, peer)` and optionally guild/team ids.
|
||||
- Direct chats collapse to `agent:<agentId>:<mainKey>` (per-agent “main”; `session.mainKey`).
|
||||
- Direct chats collapse to `agent:<agentId>:<mainKey>` (per-agent "main"; `session.mainKey`).
|
||||
|
||||
## Platform examples
|
||||
|
||||
### Discord bots per agent
|
||||
<AccordionGroup>
|
||||
<Accordion title="Discord bots per agent">
|
||||
Each Discord bot account maps to a unique `accountId`. Bind each account to an agent and keep allowlists per bot.
|
||||
|
||||
Each Discord bot account maps to a unique `accountId`. Bind each account to an agent and keep allowlists per bot.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", workspace: "~/.openclaw/workspace-main" },
|
||||
{ id: "coding", workspace: "~/.openclaw/workspace-coding" },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "main", match: { channel: "discord", accountId: "default" } },
|
||||
{ agentId: "coding", match: { channel: "discord", accountId: "coding" } },
|
||||
],
|
||||
channels: {
|
||||
discord: {
|
||||
groupPolicy: "allowlist",
|
||||
accounts: {
|
||||
default: {
|
||||
token: "DISCORD_BOT_TOKEN_MAIN",
|
||||
guilds: {
|
||||
"123456789012345678": {
|
||||
channels: {
|
||||
"222222222222222222": { allow: true, requireMention: false },
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", workspace: "~/.openclaw/workspace-main" },
|
||||
{ id: "coding", workspace: "~/.openclaw/workspace-coding" },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "main", match: { channel: "discord", accountId: "default" } },
|
||||
{ agentId: "coding", match: { channel: "discord", accountId: "coding" } },
|
||||
],
|
||||
channels: {
|
||||
discord: {
|
||||
groupPolicy: "allowlist",
|
||||
accounts: {
|
||||
default: {
|
||||
token: "DISCORD_BOT_TOKEN_MAIN",
|
||||
guilds: {
|
||||
"123456789012345678": {
|
||||
channels: {
|
||||
"222222222222222222": { allow: true, requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
coding: {
|
||||
token: "DISCORD_BOT_TOKEN_CODING",
|
||||
guilds: {
|
||||
"123456789012345678": {
|
||||
channels: {
|
||||
"333333333333333333": { allow: true, requireMention: false },
|
||||
coding: {
|
||||
token: "DISCORD_BOT_TOKEN_CODING",
|
||||
guilds: {
|
||||
"123456789012345678": {
|
||||
channels: {
|
||||
"333333333333333333": { allow: true, requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Invite each bot to the guild and enable Message Content Intent.
|
||||
- Tokens live in `channels.discord.accounts.<id>.token` (default account can use `DISCORD_BOT_TOKEN`).
|
||||
|
||||
- Invite each bot to the guild and enable Message Content Intent.
|
||||
- Tokens live in `channels.discord.accounts.<id>.token` (default account can use `DISCORD_BOT_TOKEN`).
|
||||
|
||||
### Telegram bots per agent
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", workspace: "~/.openclaw/workspace-main" },
|
||||
{ id: "alerts", workspace: "~/.openclaw/workspace-alerts" },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "main", match: { channel: "telegram", accountId: "default" } },
|
||||
{ agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } },
|
||||
],
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "123456:ABC...",
|
||||
dmPolicy: "pairing",
|
||||
},
|
||||
alerts: {
|
||||
botToken: "987654:XYZ...",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["tg:123456789"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Create one bot per agent with BotFather and copy each token.
|
||||
- Tokens live in `channels.telegram.accounts.<id>.botToken` (default account can use `TELEGRAM_BOT_TOKEN`).
|
||||
|
||||
### WhatsApp numbers per agent
|
||||
|
||||
Link each account before starting the gateway:
|
||||
|
||||
```bash
|
||||
openclaw channels login --channel whatsapp --account personal
|
||||
openclaw channels login --channel whatsapp --account biz
|
||||
```
|
||||
|
||||
`~/.openclaw/openclaw.json` (JSON5):
|
||||
|
||||
```js
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "home",
|
||||
default: true,
|
||||
name: "Home",
|
||||
workspace: "~/.openclaw/workspace-home",
|
||||
agentDir: "~/.openclaw/agents/home/agent",
|
||||
},
|
||||
{
|
||||
id: "work",
|
||||
name: "Work",
|
||||
workspace: "~/.openclaw/workspace-work",
|
||||
agentDir: "~/.openclaw/agents/work/agent",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Deterministic routing: first match wins (most-specific first).
|
||||
bindings: [
|
||||
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
|
||||
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
|
||||
|
||||
// Optional per-peer override (example: send a specific group to work agent).
|
||||
</Accordion>
|
||||
<Accordion title="Telegram bots per agent">
|
||||
```json5
|
||||
{
|
||||
agentId: "work",
|
||||
match: {
|
||||
channel: "whatsapp",
|
||||
accountId: "personal",
|
||||
peer: { kind: "group", id: "1203630...@g.us" },
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", workspace: "~/.openclaw/workspace-main" },
|
||||
{ id: "alerts", workspace: "~/.openclaw/workspace-alerts" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
|
||||
tools: {
|
||||
agentToAgent: {
|
||||
enabled: false,
|
||||
allow: ["home", "work"],
|
||||
},
|
||||
},
|
||||
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: {
|
||||
// Optional override. Default: ~/.openclaw/credentials/whatsapp/personal
|
||||
// authDir: "~/.openclaw/credentials/whatsapp/personal",
|
||||
},
|
||||
biz: {
|
||||
// Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
|
||||
// authDir: "~/.openclaw/credentials/whatsapp/biz",
|
||||
bindings: [
|
||||
{ agentId: "main", match: { channel: "telegram", accountId: "default" } },
|
||||
{ agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } },
|
||||
],
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "123456:ABC...",
|
||||
dmPolicy: "pairing",
|
||||
},
|
||||
alerts: {
|
||||
botToken: "987654:XYZ...",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["tg:123456789"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
## Example: WhatsApp daily chat + Telegram deep work
|
||||
- Create one bot per agent with BotFather and copy each token.
|
||||
- Tokens live in `channels.telegram.accounts.<id>.botToken` (default account can use `TELEGRAM_BOT_TOKEN`).
|
||||
|
||||
Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.
|
||||
</Accordion>
|
||||
<Accordion title="WhatsApp numbers per agent">
|
||||
Link each account before starting the gateway:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "chat",
|
||||
name: "Everyday",
|
||||
workspace: "~/.openclaw/workspace-chat",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
id: "opus",
|
||||
name: "Deep Work",
|
||||
workspace: "~/.openclaw/workspace-opus",
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "chat", match: { channel: "whatsapp" } },
|
||||
{ agentId: "opus", match: { channel: "telegram" } },
|
||||
],
|
||||
}
|
||||
```
|
||||
```bash
|
||||
openclaw channels login --channel whatsapp --account personal
|
||||
openclaw channels login --channel whatsapp --account biz
|
||||
```
|
||||
|
||||
Notes:
|
||||
`~/.openclaw/openclaw.json` (JSON5):
|
||||
|
||||
- If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`).
|
||||
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules.
|
||||
|
||||
## Example: same channel, one peer to Opus
|
||||
|
||||
Keep WhatsApp on the fast agent, but route one DM to Opus:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "chat",
|
||||
name: "Everyday",
|
||||
workspace: "~/.openclaw/workspace-chat",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
id: "opus",
|
||||
name: "Deep Work",
|
||||
workspace: "~/.openclaw/workspace-opus",
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
```js
|
||||
{
|
||||
agentId: "opus",
|
||||
match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } },
|
||||
},
|
||||
{ agentId: "chat", match: { channel: "whatsapp" } },
|
||||
],
|
||||
}
|
||||
```
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "home",
|
||||
default: true,
|
||||
name: "Home",
|
||||
workspace: "~/.openclaw/workspace-home",
|
||||
agentDir: "~/.openclaw/agents/home/agent",
|
||||
},
|
||||
{
|
||||
id: "work",
|
||||
name: "Work",
|
||||
workspace: "~/.openclaw/workspace-work",
|
||||
agentDir: "~/.openclaw/agents/work/agent",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Peer bindings always win, so keep them above the channel-wide rule.
|
||||
// Deterministic routing: first match wins (most-specific first).
|
||||
bindings: [
|
||||
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
|
||||
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
|
||||
|
||||
## Family agent bound to a WhatsApp group
|
||||
|
||||
Bind a dedicated family agent to a single WhatsApp group, with mention gating
|
||||
and a tighter tool policy:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "family",
|
||||
name: "Family",
|
||||
workspace: "~/.openclaw/workspace-family",
|
||||
identity: { name: "Family Bot" },
|
||||
groupChat: {
|
||||
mentionPatterns: ["@family", "@familybot", "@Family Bot"],
|
||||
// Optional per-peer override (example: send a specific group to work agent).
|
||||
{
|
||||
agentId: "work",
|
||||
match: {
|
||||
channel: "whatsapp",
|
||||
accountId: "personal",
|
||||
peer: { kind: "group", id: "1203630...@g.us" },
|
||||
},
|
||||
},
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
},
|
||||
tools: {
|
||||
allow: [
|
||||
"exec",
|
||||
"read",
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"session_status",
|
||||
],
|
||||
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
|
||||
],
|
||||
|
||||
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
|
||||
tools: {
|
||||
agentToAgent: {
|
||||
enabled: false,
|
||||
allow: ["home", "work"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: {
|
||||
// Optional override. Default: ~/.openclaw/credentials/whatsapp/personal
|
||||
// authDir: "~/.openclaw/credentials/whatsapp/personal",
|
||||
},
|
||||
biz: {
|
||||
// Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
|
||||
// authDir: "~/.openclaw/credentials/whatsapp/biz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Common patterns
|
||||
|
||||
<Tabs>
|
||||
<Tab title="WhatsApp daily + Telegram deep work">
|
||||
Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.
|
||||
|
||||
```json5
|
||||
{
|
||||
agentId: "family",
|
||||
match: {
|
||||
channel: "whatsapp",
|
||||
peer: { kind: "group", id: "120363999999999999@g.us" },
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "chat",
|
||||
name: "Everyday",
|
||||
workspace: "~/.openclaw/workspace-chat",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
id: "opus",
|
||||
name: "Deep Work",
|
||||
workspace: "~/.openclaw/workspace-opus",
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
bindings: [
|
||||
{ agentId: "chat", match: { channel: "whatsapp" } },
|
||||
{ agentId: "opus", match: { channel: "telegram" } },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
Notes:
|
||||
|
||||
- Tool allow/deny lists are **tools**, not skills. If a skill needs to run a
|
||||
binary, ensure `exec` is allowed and the binary exists in the sandbox.
|
||||
- For stricter gating, set `agents.list[].groupChat.mentionPatterns` and keep
|
||||
group allowlists enabled for the channel.
|
||||
- If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`).
|
||||
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules.
|
||||
|
||||
## Per-Agent Sandbox and Tool Configuration
|
||||
</Tab>
|
||||
<Tab title="Same channel, one peer to Opus">
|
||||
Keep WhatsApp on the fast agent, but route one DM to Opus:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "chat",
|
||||
name: "Everyday",
|
||||
workspace: "~/.openclaw/workspace-chat",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
id: "opus",
|
||||
name: "Deep Work",
|
||||
workspace: "~/.openclaw/workspace-opus",
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "opus",
|
||||
match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } },
|
||||
},
|
||||
{ agentId: "chat", match: { channel: "whatsapp" } },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Peer bindings always win, so keep them above the channel-wide rule.
|
||||
|
||||
</Tab>
|
||||
<Tab title="Family agent bound to a WhatsApp group">
|
||||
Bind a dedicated family agent to a single WhatsApp group, with mention gating and a tighter tool policy:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "family",
|
||||
name: "Family",
|
||||
workspace: "~/.openclaw/workspace-family",
|
||||
identity: { name: "Family Bot" },
|
||||
groupChat: {
|
||||
mentionPatterns: ["@family", "@familybot", "@Family Bot"],
|
||||
},
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
scope: "agent",
|
||||
},
|
||||
tools: {
|
||||
allow: [
|
||||
"exec",
|
||||
"read",
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"session_status",
|
||||
],
|
||||
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "family",
|
||||
match: {
|
||||
channel: "whatsapp",
|
||||
peer: { kind: "group", id: "120363999999999999@g.us" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Tool allow/deny lists are **tools**, not skills. If a skill needs to run a binary, ensure `exec` is allowed and the binary exists in the sandbox.
|
||||
- For stricter gating, set `agents.list[].groupChat.mentionPatterns` and keep group allowlists enabled for the channel.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Per-agent sandbox and tool configuration
|
||||
|
||||
Each agent can have its own sandbox and tool restrictions:
|
||||
|
||||
@@ -591,25 +587,26 @@ Each agent can have its own sandbox and tool restrictions:
|
||||
}
|
||||
```
|
||||
|
||||
Note: `setupCommand` lives under `sandbox.docker` and runs once on container creation.
|
||||
Per-agent `sandbox.docker.*` overrides are ignored when the resolved scope is `"shared"`.
|
||||
<Note>
|
||||
`setupCommand` lives under `sandbox.docker` and runs once on container creation. Per-agent `sandbox.docker.*` overrides are ignored when the resolved scope is `"shared"`.
|
||||
</Note>
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Security isolation**: Restrict tools for untrusted agents
|
||||
- **Resource control**: Sandbox specific agents while keeping others on host
|
||||
- **Flexible policies**: Different permissions per agent
|
||||
- **Security isolation**: restrict tools for untrusted agents.
|
||||
- **Resource control**: sandbox specific agents while keeping others on host.
|
||||
- **Flexible policies**: different permissions per agent.
|
||||
|
||||
Note: `tools.elevated` is **global** and sender-based; it is not configurable per agent.
|
||||
If you need per-agent boundaries, use `agents.list[].tools` to deny `exec`.
|
||||
For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent.
|
||||
<Note>
|
||||
`tools.elevated` is **global** and sender-based; it is not configurable per agent. If you need per-agent boundaries, use `agents.list[].tools` to deny `exec`. For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent.
|
||||
</Note>
|
||||
|
||||
See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for detailed examples.
|
||||
See [Multi-agent sandbox and tools](/tools/multi-agent-sandbox-tools) for detailed examples.
|
||||
|
||||
## Related
|
||||
|
||||
- [Channel Routing](/channels/channel-routing) — how messages route to agents
|
||||
- [Sub-Agents](/tools/subagents) — spawning background agent runs
|
||||
- [ACP Agents](/tools/acp-agents) — running external coding harnesses
|
||||
- [ACP agents](/tools/acp-agents) — running external coding harnesses
|
||||
- [Channel routing](/channels/channel-routing) — how messages route to agents
|
||||
- [Presence](/concepts/presence) — agent presence and availability
|
||||
- [Session](/concepts/session) — session isolation and routing
|
||||
- [Sub-agents](/tools/subagents) — spawning background agent runs
|
||||
|
||||
Reference in New Issue
Block a user