OpenClaw
Point OpenClaw at the gateway. Config alone is enough, so it works in a hardened container with nothing installed into it.
Everything shared (self-hosting, how the gateway reaches providers) is documented once in the gateway.
Sandboxed OpenClaw: Grok on Oracle OCI
The common secured setup: OpenClaw runs in a locked-down container (read-only rootfs, no npm, egress allowlist), and inference goes to xAI Grok hosted on Oracle Cloud Infrastructure Generative AI. You cannot install anything into that container, and you do not need to. Four things do it: a provider entry, a default model, one environment variable, and the gateway on the egress allowlist.
Oracle credentials stay server-side on the gateway, so the container never holds them. It
holds only its own ark_… client key.
Console Providers page (or PUT /admin/provider-keys). oracleRegion is required and
has no default: OCI's host is region-scoped, so a guessed region cannot produce a working
request. For production use IAM request signing, which has no API key at all. Full field
list: Choose your setup.
Console Routing page, or a config from Routing examples below.
Match it to the variant you picked: single mode for the Grok-only config, and
dispatch by requested model for any config whose menu spans vendors. Without this the
container has to name the provider itself on every request.
Write the file below to ~/.openclaw/openclaw.json, or wherever OPENCLAW_CONFIG_PATH
points. A read-only mount is fine.
Console Users page → Agents → New agent key. A container is not a person, so
this is the right key type: it mints an ark_svc_… service key, attributed to the agent
name you give it. See Which key to use.
Set ANYRAY_CLIENT_KEY on the container to that value. OpenClaw substitutes
${ANYRAY_CLIENT_KEY} at load, so no credential is written to disk and rotating the key is
a restart, not a config edit.
Your gateway host is the only host inference needs. OpenClaw still reaches other hosts for its own features (a model-catalog refresh at boot, any chat channels you connect), so allowlist by what OpenClaw needs, not by this route alone.
Run openclaw config validate. It names any unresolved environment variable, so a missing
key surfaces as a warning rather than a runtime 401.
Pick the variant that matches your providers. All of them are complete files, and every one keeps
credentials out of the container except its own ark_… key.
- Grok on Oracle OCI
- Grok direct from xAI
- Claude Opus and Sonnet
- GPT (needs its own entry)
- Everything: Opus, Sonnet, Grok, GPT
- Stable aliases, no vendor ids
- Two entries, one provider each
{
models: {
providers: {
anyray: {
// Bare origin, no `/v1`: the anthropic-messages transport appends
// `/v1/messages` itself.
baseUrl: 'https://gateway.example.anyray.ai',
// Without the explicit api kind OpenClaw serves the entry over
// openai-completions: wrong dialect, wrong path.
api: 'anthropic-messages',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
// OpenClaw drops its implicit beta headers on a custom host.
'anthropic-beta': 'interleaved-thinking-2025-05-14',
// Labels this traffic as OpenClaw. User and team come from the key,
// not from here.
'x-anyray-metadata': '{"tool":"openclaw"}',
},
// A provider OpenClaw does not ship must list its own models. OCI keeps
// its dotted vendor prefix on the wire, so the id is `xai.grok-4.3`,
// not `grok-4.3`.
models: [
{
id: 'xai.grok-4.3',
name: 'Grok 4.3 (Anyray)',
reasoning: true,
input: ['text'],
contextWindow: 1000000,
maxTokens: 128000,
},
],
},
},
},
agents: {
// Routing stays inert until the primary model resolves to a routed
// provider.
defaults: { model: { primary: 'anyray/xai.grok-4.3' } },
},
}
{
models: {
providers: {
anyray: {
baseUrl: 'https://gateway.example.anyray.ai',
api: 'anthropic-messages',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
// xAI's own ids, with NO dotted prefix. Same models as OCI serves,
// different namespace and a different provider key on the gateway.
models: [
{
id: 'grok-4.6',
name: 'Grok 4.6',
reasoning: true,
input: ['text'],
contextWindow: 500000,
maxTokens: 128000,
},
{
id: 'grok-build-0.1',
name: 'Grok Build 0.1 (cheap coding)',
reasoning: true,
input: ['text'],
contextWindow: 256000,
maxTokens: 128000,
},
],
},
},
},
agents: {
defaults: {
model: { primary: 'anyray/grok-4.6', fallbacks: ['anyray/grok-build-0.1'] },
},
},
}
{
models: {
providers: {
anyray: {
baseUrl: 'https://gateway.example.anyray.ai',
api: 'anthropic-messages',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'anthropic-beta': 'interleaved-thinking-2025-05-14',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
// maxTokens is the model's OUTPUT ceiling, and it differs by family:
// 32000 for opus, 64000 for sonnet. Over-stating it is a 400.
models: [
{
id: 'claude-opus-5',
name: 'Claude Opus 5',
reasoning: true,
input: ['text', 'image'],
contextWindow: 1000000,
maxTokens: 32000,
},
{
id: 'claude-sonnet-5',
name: 'Claude Sonnet 5',
reasoning: true,
input: ['text', 'image'],
contextWindow: 1000000,
maxTokens: 64000,
},
],
},
},
},
agents: {
defaults: {
// Sonnet primary, Opus for the work that needs it (`/model
// anyray/claude-opus-5` per conversation).
model: {
primary: 'anyray/claude-sonnet-5',
fallbacks: ['anyray/claude-opus-5'],
},
// Never "none": that strips every cache_control marker, so each warm
// turn re-reads the whole prefix at full price.
params: { cacheRetention: 'short' },
},
},
}
{
models: {
providers: {
// GPT CANNOT share an anthropic-messages entry with Claude or Grok. The
// gateway's `openai` provider serves no /v1/messages endpoint, so a GPT
// id sent on that wire does not route. It needs this second entry, on
// the OpenAI wire, with the `/v1` base that transport expects.
'anyray-gpt': {
baseUrl: 'https://gateway.example.anyray.ai/v1',
api: 'openai-completions',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'x-anyray-provider': 'openai',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
models: [
{
id: 'gpt-5.5',
name: 'GPT-5.5',
reasoning: true,
input: ['text', 'image'],
contextWindow: 1050000,
maxTokens: 65536,
},
],
},
},
},
agents: {
defaults: { model: { primary: 'anyray-gpt/gpt-5.5' } },
},
}
{
models: {
providers: {
// TWO entries, because the wires differ. Claude and Grok share the
// anthropic-messages entry (the gateway translates Grok to xAI's chat
// path); GPT needs the OpenAI wire, since `openai` serves no
// /v1/messages. Both point at the same gateway and the same key.
//
// REQUIRES the "Dispatch by requested model" routing config below. This
// entry pins no provider, and the gateway has no model->provider map, so
// without that config a Claude id and a Grok id are indistinguishable to
// it and both go wherever the default points.
anyray: {
baseUrl: 'https://gateway.example.anyray.ai',
api: 'anthropic-messages',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'anthropic-beta': 'interleaved-thinking-2025-05-14',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
models: [
{
id: 'claude-sonnet-5',
name: 'Claude Sonnet 5',
reasoning: true,
input: ['text', 'image'],
contextWindow: 1000000,
maxTokens: 64000,
},
{
id: 'claude-opus-5',
name: 'Claude Opus 5',
reasoning: true,
input: ['text', 'image'],
contextWindow: 1000000,
maxTokens: 32000,
},
{
id: 'xai.grok-4.3',
name: 'Grok 4.3 (OCI)',
reasoning: true,
input: ['text'],
contextWindow: 1000000,
maxTokens: 128000,
},
],
},
'anyray-gpt': {
baseUrl: 'https://gateway.example.anyray.ai/v1',
api: 'openai-completions',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'x-anyray-provider': 'openai',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
models: [
{
id: 'gpt-5.5',
name: 'GPT-5.5',
reasoning: true,
input: ['text', 'image'],
contextWindow: 1050000,
maxTokens: 65536,
},
],
},
},
},
agents: {
defaults: {
model: {
primary: 'anyray/claude-sonnet-5',
fallbacks: ['anyray/xai.grok-4.3'],
},
params: { cacheRetention: 'short' },
},
},
}
{
models: {
providers: {
anyray: {
baseUrl: 'https://gateway.example.anyray.ai',
api: 'anthropic-messages',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'anthropic-beta': 'interleaved-thinking-2025-05-14',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
// These ids are ALIASES registered on the gateway, not vendor models.
// Re-pointing `frontier` at another vendor is an admin change, and no
// machine is edited. See "Stable aliases" below.
models: [
{
id: 'frontier',
name: 'Frontier',
reasoning: true,
input: ['text'],
contextWindow: 1000000,
maxTokens: 128000,
},
{
id: 'fast',
name: 'Fast',
input: ['text'],
contextWindow: 131072,
maxTokens: 32000,
},
],
},
},
},
agents: {
defaults: {
model: { primary: 'anyray/frontier', fallbacks: ['anyray/fast'] },
params: { cacheRetention: 'short' },
},
},
}
{
models: {
providers: {
// Each entry names its gateway provider outright, so the model picker
// shows the vendor. An explicit provider header WINS over the org's
// routing config, so these two bypass it.
'anyray-oci': {
baseUrl: 'https://gateway.example.anyray.ai',
api: 'anthropic-messages',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'x-anyray-provider': 'oracle',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
models: [
{
id: 'xai.grok-4.3',
name: 'Grok 4.3 (OCI)',
reasoning: true,
input: ['text'],
contextWindow: 1000000,
maxTokens: 128000,
},
],
},
'anyray-claude': {
baseUrl: 'https://gateway.example.anyray.ai',
api: 'anthropic-messages',
apiKey: '${ANYRAY_CLIENT_KEY}',
headers: {
'x-anyray-api-key': '${ANYRAY_CLIENT_KEY}',
'x-anyray-provider': 'anthropic',
'anthropic-beta': 'interleaved-thinking-2025-05-14',
'x-anyray-metadata': '{"tool":"openclaw"}',
},
models: [
{
id: 'claude-sonnet-5',
name: 'Claude Sonnet 5',
reasoning: true,
input: ['text', 'image'],
contextWindow: 200000,
maxTokens: 64000,
},
],
},
},
},
agents: {
defaults: {
model: { primary: 'anyray-oci/xai.grok-4.3' },
params: { cacheRetention: 'short' },
},
},
}
api and baseUrl go together. anthropic-messages takes a bare origin and appends
/v1/messages; openai-completions takes a /v1-suffixed base and appends
/chat/completions. Omit api entirely and OpenClaw serves the entry over openai-completions
against a bare origin, which is both the wrong dialect and a path the gateway 404s. Prefer
anthropic-messages where the model supports it: it carries cache_control and thinking
blocks that the OpenAI wire cannot express.
Claude and Grok can, GPT cannot. The gateway serves /v1/messages for anthropic natively and
translates it for oracle, x-ai, nebius, litellm and truefoundry, but the openai
provider has no /v1/messages endpoint at all. So an org running Opus, Sonnet, Grok and GPT
needs exactly two provider entries: one anthropic-messages entry carrying Claude and Grok, and
one openai-completions entry for GPT. The Everything tab above is that shape.
The gateway has no model-to-provider map. It never infers oracle from xai.grok-4.3 or
anthropic from claude-sonnet-5. A provider entry listing models from several vendors, with no
x-anyray-provider pinned, therefore needs one of these or it will not work:
- A conditional routing config keyed on
params.model(the recommended shape, and what the Everything tab assumes). Dispatch is then central and the machines never change. - One entry per vendor, each pinning
x-anyray-provider(the Two entries tab). Dispatch is decided on the machine, and the org's routing config is bypassed for those entries.
Get it wrong and the failure is not a clear error. With a single-mode config every model in the
menu goes to that one provider, so a claude-sonnet-5 request lands on Oracle and comes back as
an upstream 404 for an unknown model. With no routing config and more than one provider key
stored, the gateway cannot pick a default at all and every request fails the same way:
Either x-anyray-config or x-anyray-provider header is required. A single stored provider key is
the only case that silently works, because it is then the unambiguous default.
Attribution, spend governance, and per-conversation optimizer sessions all work on this route.
Spend is attributed from the ark_ key's own bound user and team, so a personal key needs no
header to be counted correctly. Sessions resolve per conversation because OpenClaw stamps a
unique sessionId into its own system prompt, which is what the gateway fingerprints.
x-anyray-metadata is honor-system client text, so the gateway lets the verified key override
it. Whatever user and team you put in that header are replaced by the key's own values, and
a key with no team clears a header-supplied one rather than letting a holder file their spend
under another team. Send tool there, which is kept, and set identity by minting the key
against the right user. A shared key attributes every container that holds it to one identity.
Which key to use
ANYRAY_CLIENT_KEY is an Anyray client key, minted in the console. There are two kinds, and
for a container you want the second.
Personal key (ark_…) | Agent key (ark_svc_…) | |
|---|---|---|
| Minted at | Users page, by enrolling a person (or anyray-connect --enroll) | Users page → Agents → New agent key |
| Attributed to | That person, and their team | The agent name you set at mint time |
| Use it for | A developer's own machine | A container, a CI job, an unattended service |
| Expiry | Sliding: renewed on use, so an active developer never hits a mid-session 401 | Whatever you set, as a hard limit |
| Billed seat | Yes, counts as an active seat | No, excluded from the seat count automatically |
| Extras | Follows the person's team for team-scoped routing | Optional monthly USD budget cap |
Both authenticate identically (ark_svc_ is an ark_ key), and both ride the same
x-anyray-api-key header the configs above use, so nothing else in the file changes.
openclaw-prod, not agent1). Letters, digits, dot, colon, hyphen and underscore only, up to 64 characters.Anyone who can read the container's environment can use it. Give each deployment its own key
rather than sharing one, since a shared key files every container's spend under one identity and
makes revocation all-or-nothing. Rotating is a new key plus a restart: nothing in
openclaw.json changes, because the file references ${ANYRAY_CLIENT_KEY} rather than the
value.
Anyray bills $10/month per active seat, and an agent key is excluded from that count automatically, with nothing to hand-configure. Its usage is still fully metered and attributed, it just does not count as a person. A container running on a developer's personal key does the opposite: it files that container's spend under a human who is not doing the work, and keeps that person counted as an active seat even in a month when they never opened a terminal.
A personal key is sliding-renewable: the gateway extends its expiry on use, which is what stops a working developer hitting a mid-session 401. In an unattended container that same property means a credential that quietly never expires. An agent key's expiry is a hard limit, and it can carry a monthly USD cap.
Which OCI models to list
OCI fronts four vendors behind one provider slug, and every model keeps OCI's dotted vendor prefix on the wire. Any of them routes; what differs is how exactly Anyray can price it.
xAI Grok. Oracle prices these per million tokens, so they convert exactly.
| Model id | Context | Priced |
|---|---|---|
xai.grok-4.3 | 1M | Exact |
xai.grok-4.20 | 1M | Exact |
xai.grok-4.2 | 1M | Exact |
xai.grok-4-1-fast | 2M | Exact |
OpenAI open-weights. Also per-token, also exact.
| Model id | Context | Priced |
|---|---|---|
openai.gpt-oss-120b | 131k | Exact |
openai.gpt-oss-20b | 128k | Exact |
Meta Llama and Cohere Command. These route and meter normally, but price at a fallback rate (see the warning below). The ids below are the ones this repo has verified; Oracle's catalog moves faster than any list here, so treat the OCI model reference as authoritative and send any id it lists.
| Model id | Priced |
|---|---|
meta.llama-4-scout-17b-16e-instruct | Fallback rate |
meta.llama-3.3-70b-instruct | Fallback rate |
meta.llama-3.1-405b-instruct | Fallback rate |
cohere.command-a-03-2025 | Fallback rate |
cohere.command-a-vision | Fallback rate |
cohere.command-r-08-2024 | Fallback rate |
OCI shares the dotted namespace with other providers, and its id is a prefix of theirs:
Vertex serves meta.llama-3.3-70b-instruct-maas, Bedrock serves cohere.command-r-plus-v1:0.
Same family, different upstream, different price. Send an OCI id only to the oracle provider,
and let the routing config decide the provider rather than guessing from the id's shape.
To list one of these in openclaw.json, copy any tab above and swap the id. Take
contextWindow and maxTokens from Oracle's model reference for that model rather than reusing
a Grok entry's: maxTokens must be positive or the Messages transport refuses to run, and an
over-stated contextWindow means OpenClaw packs a prompt the model then rejects.
Oracle bills those two families per character rather than per token, and publishes no
characters-per-token ratio, so Anyray prices them at a fallback rate. They route and meter
normally, and each row carries a priceSource recording the fallback, but take Oracle's own
invoice as authoritative for them.
Oracle publishes a cached-input rate for the Grok ids, and the gateway detects that from the
model id through the dotted prefix, so warm prefixes are priced at the read tier. Oracle
documents no automatic prompt caching for meta.llama-*, cohere.*, or openai.gpt-oss-*, so
the optimizer's cache-busting strategies stay free to run on those. Nothing to configure either
way.
Two layers of routing
Routing happens twice, and knowing which layer owns what saves a lot of guessing. OpenClaw picks a model ref; the gateway picks the provider and the model id that reaches it.
| OpenClaw config | Gateway routing | |
|---|---|---|
| Lives in | openclaw.json, on the machine | Console Routing / PUT /admin/routing-config, server-side |
| Decides | Which provider entry and model ref a conversation uses | Which upstream provider serves it, and under which model id |
| Changed by | Editing the file (or /model), per machine | An admin, once, for every client at once |
| Holds credentials | The ark_… client key only | The provider keys (Oracle, xAI, Anthropic) |
| Sees | Nothing about upstreams | Fallback, load balance, conditions, retry |
The practical split: put in openclaw.json only what a user would legitimately choose (a fast
model versus a frontier one), and leave which vendor serves that choice to the gateway. An
OpenClaw config that names providers directly has to be re-edited on every machine when you
switch vendors; a gateway routing config is one admin change.
An x-anyray-provider header on a provider entry wins over the org's routing config, and an
x-anyray-config header wins over both. When a request carries neither, the gateway applies the
default routing config; if none is stored and exactly one provider key is configured, that sole
provider is used. So a container that sends no Anyray routing headers is fully steered by the
console, which is usually what you want for a fleet.
Routing examples
Set these on the console Routing page, or PUT /admin/routing-config. A client can override
per request with the x-anyray-config header. Oracle needs no api_key in the config: the
region and credential come from the server-held provider bag, and the IAM signing path has no
key at all. Full semantics: Configure → Routing.
- Single: everything to Oracle
- Fallback: Oracle, then xAI direct
- Conditional: by team
- Load balance: mostly Grok, some gpt-oss
- Dispatch by requested model, across vendors
- Cross-vendor fallback chain
{
"strategy": { "mode": "single" },
"targets": [{ "provider": "oracle" }]
}
{
"strategy": { "mode": "fallback", "on_status_codes": [429, 500, 502, 503] },
"targets": [
{ "provider": "oracle" },
{ "provider": "x-ai", "override_params": { "model": "grok-4.3" } }
]
}
{
"strategy": {
"mode": "conditional",
"conditions": [{ "query": { "metadata.team": "research" }, "then": "frontier" }],
"default": "economy"
},
"targets": [
{ "name": "frontier", "provider": "oracle",
"override_params": { "model": "xai.grok-4.3" } },
{ "name": "economy", "provider": "oracle",
"override_params": { "model": "openai.gpt-oss-120b" } }
]
}
{
"strategy": { "mode": "loadbalance" },
"targets": [
{ "provider": "oracle", "weight": 3,
"override_params": { "model": "xai.grok-4.3" } },
{ "provider": "oracle", "weight": 1,
"override_params": { "model": "openai.gpt-oss-120b" } }
]
}
{
"strategy": {
"mode": "conditional",
"conditions": [
{ "query": { "params.model": { "$regex": "grok" } }, "then": "oci-grok" },
{ "query": { "params.model": { "$regex": "^gpt" } }, "then": "gpt" },
{ "query": { "params.model": { "$regex": "^claude-opus" } }, "then": "opus" },
{ "query": { "params.model": { "$regex": "^claude" } }, "then": "sonnet" }
],
"default": "sonnet"
},
"targets": [
{ "name": "oci-grok", "provider": "oracle",
"override_params": { "model": "xai.grok-4.3" } },
{ "name": "gpt", "provider": "openai",
"override_params": { "model": "gpt-5.5" } },
{ "name": "opus", "provider": "anthropic",
"override_params": { "model": "claude-opus-5" } },
{ "name": "sonnet", "provider": "anthropic",
"override_params": { "model": "claude-sonnet-5" } }
]
}
{
"strategy": { "mode": "fallback", "on_status_codes": [429, 500, 502, 503] },
"targets": [
{ "provider": "oracle", "override_params": { "model": "xai.grok-4.3" } },
{ "provider": "x-ai", "override_params": { "model": "grok-4.3" } },
{ "provider": "anthropic",
"override_params": { "model": "claude-sonnet-5" } }
],
"retry": { "attempts": 2 }
}
Every provider slug above is one Anyray serves directly (oracle, x-ai, anthropic, openai,
bedrock, vertex-ai, google, groq, deepseek, nebius, openrouter, and more). Each
target draws its own credential from the server-held store, so one config can span vendors
without a single key in the file.
Model ids do not transfer between vendors. xai.grok-4.3 is an OCI id, grok-4.3 is xAI's own,
and neither means anything to Anthropic, so a fallback chain without override_params.model on
each target fails at the second hop with an upstream 404 rather than failing over. This is the
single most common mistake in a multi-vendor config.
Four more things decide whether these behave as written:
- First match wins, so the specific condition goes first.
^claude-opusmust precede^claude, or every Opus request is served by the Sonnet target. For the same reason the Grok arm matchesgrokunanchored:^grokwould miss OCI'sxai.grok-4.3entirely, because the dotted vendor prefix sits at the start of the id. - Identity fields are evaluated on the verified key-bound identity.
metadata.userandmetadata.teamcannot be steered by editingx-anyray-metadata, so a team lane needs team-bearing keys (SSO enrollment). Every othermetadata.<key>stays client-controlled, andparams.<field>(theparams.modelconditions above) reads the request body. - Routing applies to API-key traffic only.
passthroughsubscription traffic keeps the user's own token and provider. - A sole configured provider key is already the default, so a single-provider deployment can skip routing config entirely and the container still needs no Anyray headers.
Stable aliases
The one pattern the tabs above cannot show on their own. Register aliases centrally (console, or
PUT /admin/model-aliases), and the Stable aliases variant becomes a config no vendor id ever
appears in: re-pointing frontier at another vendor is one admin change, and no machine is
edited. The lowest-churn option for a managed fleet.
{
"frontier": "oracle/xai.grok-4.3",
"fast": "oracle/openai.gpt-oss-120b",
"claude": "anthropic/claude-sonnet-5"
}
The gateway rewrites the body model and names the resolved provider, so an alias resolves only
when the client sent no explicit provider. That makes aliases and the Two entries, one provider
each variant mutually exclusive: a pinned x-anyray-provider wins, and the alias is ignored.
contextWindow and maxTokens still describe whatever the alias points at, so re-pointing one
across a large context-window gap means revisiting those numbers.
Other ways to connect
anyray-connect (unsandboxed machines)
Where you can install into the machine, Connect writes the whole config for you:
npx anyray-connect@latest --gateway http://<gateway>:8787 --tools openclaw
Enroll first with --enroll <link> (or --sso); Connect makes no changes without a valid
personal ark_… key. Use --dry-run to preview.
Connect merges Anyray-owned keys into your existing config and keeps everything else, comments included. It writes:
- The providers.
models.providers.anthropicgets the gateway origin asbaseUrl(OpenClaw appends/v1/messagesitself), plusapi: "anthropic-messages"and amaxTokensvalue. Both are required: a customized entry no longer inherits them from OpenClaw's built-in catalog, and without them requests silently use the wrong dialect.models.providers.openaigetshttp://<gateway>:8787/v1. - Your key. Your personal
ark_…key ridesapiKeyand thex-anyray-api-keyheader. - A default model, only if you have none. A missing
agents.defaults.model.primaryis seeded toanthropic/claude-sonnet-4-5, with fallbackanthropic/claude-haiku-4-5when you have nofallbackskey at all. A primary or fallback pointing at an unrouted provider (sayopenrouter/…) is kept but reported, because those requests bypass the gateway. - Prompt caching and beta headers. Connect pins
agents.defaults.params.cacheRetention: "short"unless you already set a value, and restores the beta headers OpenClaw drops on a custom host.
Connect backs the file up before its first write. --revert removes only what Connect wrote.
OpenClaw hot-reloads model config, so no restart is needed. Run openclaw config validate to
confirm the merged file passes OpenClaw's strict schema.
The Anyray plugin (per-conversation sessions, Claude and xAI-direct ids)
Connect also installs the Anyray plugin (@anyray/openclaw-plugin) at
~/.anyray/openclaw-plugin and registers it under plugins.load.paths and
plugins.entries.anyray. It contributes the whole provider entry from one config block,
stamps an opaque session id per call, and sets cache retention in code.
Its catalog carries Claude ids plus xAI-direct Grok ids (grok-4.6, grok-4.3,
grok-build-0.1), and it names the x-ai provider on those calls itself. Those are the
xAI-direct ids, not the OCI ones: OCI's xai.grok-4.3 is a different id served by the
oracle provider, and the plugin leaves it to your routing config. The Claude ids stay
unstamped, so your default routing stays in charge of them.
Restart the OpenClaw gateway once after the first install so the plugin loads; config-only
changes hot-reload as usual. Then run openclaw config validate and pick anyray/* models.
The plugin source is public and MIT-licensed at anyrayHQ/openclaw-plugin, so you can read exactly what it sends before you run it. Connect ships its own embedded copy, so machines with restricted egress need nothing fetched separately.
The plugin imports plugin-sdk/provider-entry, which landed in 2026.7.1. On older builds the
import fails at load and the gateway logs the plugin as unavailable. Routing through
models.providers entries still works on those versions.
0.1.1 and earlier stamped every request as {"tool":"openclaw"}, dropping the user and
team from your plugin config, so that traffic reached the spend store unattributed. 0.2.0
sends the configured values. Connect-installed copies were never affected.
Claude models instead of Grok
Same config shape, different ids and one extra consideration. Point the models list at
claude-sonnet-5 or claude-opus-5 (and route to anthropic or bedrock), and never set
cacheRetention: "none": it strips every cache_control marker, so each warm turn re-reads the
whole prefix at full price. The Anthropic transport already resolves to "short" when nothing
is set, so leaving it unset is safe.
Routing Claude to Oracle is the one combination to avoid. The gateway serves /v1/messages
to OCI by translating it into OpenAI chat, which carries no cache_control breakpoint and drops
thinking blocks, so a warm Claude session loses its prompt cache. Grok on OCI is unaffected:
it has no cache_control to lose and caches implicitly from the model id.
Claude subscription stays native
OpenClaw can ride a personal Claude subscription (a pasted setup token, or its claude-cli
runtime, which spawns Claude Code). Connect holds a detected Claude seat on that native lane
rather than silently moving its inference onto the org API bill; pass --org --tools openclaw
to opt in explicitly. When a seat appears on a machine Connect had already routed, the stale
gateway route is removed.
Seat detection reads the machine's Claude sign-in state, not OpenClaw's own credential store:
a setup token pasted only into OpenClaw, on a machine with no Claude Code sign-in, is not
visible to Connect. If that is your setup, skip openclaw in the sweep (or accept that
routing it moves that usage to org billing).
Troubleshooting
| Symptom | Cause |
|---|---|
| Requests 404, or the optimizer never fires | The provider entry has no explicit api: 'anthropic-messages', so OpenClaw served it over openai-completions. |
expected array, received undefined at boot | A customized provider entry must list its own models; it no longer inherits OpenClaw's catalog. |
| Nothing routes, no errors | Routing is inert until a model ref resolves to the provider. Set agents.defaults.model.primary, or /model anyray/xai.grok-4.3. |
401 naming an Oracle credential | No Oracle credential stored on the gateway. It fails closed rather than forwarding your ark_ key upstream. |
400 naming oracleRegion | The region is mandatory and has no default. |
| A new optimizer session mid-conversation | Switching model or thinking level changes OpenClaw's own Runtime line, which the fingerprint covers. Costs one re-decided prefix, nothing after. |