Skip to main content

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.

1
Store the Oracle credential on the gateway

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.

2
Point routing at your providers

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.

3
Mount the config

Write the file below to ~/.openclaw/openclaw.json, or wherever OPENCLAW_CONFIG_PATH points. A read-only mount is fine.

4
Mint the key

Console Users page → AgentsNew 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.

5
Pass the key as an environment variable

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.

6
Allow the gateway through egress

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.

7
Validate

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
{
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' } },
},
}
Two fields decide whether any of these work

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.

Which models can share one entry

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.

A multi-vendor menu does not route itself

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.

What you get with no plugin and no installs

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.

The key sets user and team, not the header

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 atUsers page, by enrolling a person (or anyray-connect --enroll)Users page → AgentsNew agent key
Attributed toThat person, and their teamThe agent name you set at mint time
Use it forA developer's own machineA container, a CI job, an unattended service
ExpirySliding: renewed on use, so an active developer never hits a mid-session 401Whatever you set, as a hard limit
Billed seatYes, counts as an active seatNo, excluded from the seat count automatically
ExtrasFollows the person's team for team-scoped routingOptional 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.

1
Open Users → Agents
Console Users page, Agents card.
2
Name the agent
The name becomes the attributed user id in spend, so use something you will recognize in the dashboard (openclaw-prod, not agent1). Letters, digits, dot, colon, hyphen and underscore only, up to 64 characters.
3
Set a team and, optionally, a budget
The team drives team-scoped routing and reporting. A monthly USD cap is available for a key you want bounded.
4
Copy it once
The raw key is shown exactly once and is never re-fetchable. Put it straight into the container's secret store.
An agent key is a bearer credential in an environment variable

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.

An agent key is not a billed seat

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.

The other reason to prefer an agent key

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 idContextPriced
xai.grok-4.31MExact
xai.grok-4.201MExact
xai.grok-4.21MExact
xai.grok-4-1-fast2MExact

OpenAI open-weights. Also per-token, also exact.

Model idContextPriced
openai.gpt-oss-120b131kExact
openai.gpt-oss-20b128kExact

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 idPriced
meta.llama-4-scout-17b-16e-instructFallback rate
meta.llama-3.3-70b-instructFallback rate
meta.llama-3.1-405b-instructFallback rate
cohere.command-a-03-2025Fallback rate
cohere.command-a-visionFallback rate
cohere.command-r-08-2024Fallback rate
These ids look like Bedrock and Vertex ids, and are not

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.

Meta and Cohere spend is indicative, not exact

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.

Grok on OCI caches; the other OCI families do not

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 configGateway routing
Lives inopenclaw.json, on the machineConsole Routing / PUT /admin/routing-config, server-side
DecidesWhich provider entry and model ref a conversation usesWhich upstream provider serves it, and under which model id
Changed byEditing the file (or /model), per machineAn admin, once, for every client at once
Holds credentialsThe ark_… client key onlyThe provider keys (Oracle, xAI, Anthropic)
SeesNothing about upstreamsFallback, 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.

Precedence, exactly

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
{
"strategy": { "mode": "single" },
"targets": [{ "provider": "oracle" }]
}

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.

A cross-vendor fallback must rewrite the model

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-opus must precede ^claude, or every Opus request is served by the Sonnet target. For the same reason the Grok arm matches grok unanchored: ^grok would miss OCI's xai.grok-4.3 entirely, because the dotted vendor prefix sits at the start of the id.
  • Identity fields are evaluated on the verified key-bound identity. metadata.user and metadata.team cannot be steered by editing x-anyray-metadata, so a team lane needs team-bearing keys (SSO enrollment). Every other metadata.<key> stays client-controlled, and params.<field> (the params.model conditions above) reads the request body.
  • Routing applies to API-key traffic only. passthrough subscription 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.

PUT /admin/model-aliases
{
"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.anthropic gets the gateway origin as baseUrl (OpenClaw appends /v1/messages itself), plus api: "anthropic-messages" and a maxTokens value. 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.openai gets http://<gateway>:8787/v1.
  • Your key. Your personal ark_… key rides apiKey and the x-anyray-api-key header.
  • A default model, only if you have none. A missing agents.defaults.model.primary is seeded to anthropic/claude-sonnet-4-5, with fallback anthropic/claude-haiku-4-5 when you have no fallbacks key at all. A primary or fallback pointing at an unrouted provider (say openrouter/…) 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.

Requires OpenClaw 2026.7.1 or newer

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.

Upgrade past 0.1.x if you installed from npm

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

SymptomCause
Requests 404, or the optimizer never firesThe provider entry has no explicit api: 'anthropic-messages', so OpenClaw served it over openai-completions.
expected array, received undefined at bootA customized provider entry must list its own models; it no longer inherits OpenClaw's catalog.
Nothing routes, no errorsRouting 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 credentialNo Oracle credential stored on the gateway. It fails closed rather than forwarding your ark_ key upstream.
400 naming oracleRegionThe region is mandatory and has no default.
A new optimizer session mid-conversationSwitching model or thinking level changes OpenClaw's own Runtime line, which the fingerprint covers. Costs one re-decided prefix, nothing after.