OpenClaw Setup: Custom OpenAI-Compatible Provider
OpenClaw ignores the base URL environment variables you expect. Here is the provider block in openclaw.json that actually works, plus the model policy gotcha.
OpenClaw is a self-hosted personal assistant that runs as a local gateway daemon, connecting a model to your files, messaging channels, browser and MCP tools. It is not primarily a coding CLI — it orchestrates coding agents rather than being one, launching them as background workers.
Pointing it at a custom OpenAI-compatible endpoint is straightforward once you know the shape of the configuration. It is frustrating beforehand, because the environment variables everyone reaches for first do not work.
The environment variable trap
Start here, because it saves an hour. OpenClaw does not honour OPENAI_BASE_URL or ANTHROPIC_BASE_URL. Setting them has no effect on where requests go, and there are open issue reports of exactly this failing quietly — the gateway ignores the variable and keeps talking to the built-in endpoint, producing zero traffic through your proxy while appearing to work.
It goes further than not reading them. OpenClaw actively blocks keys ending in _BASE_URL, _ENDPOINT, _API_HOST and _HOMESERVER from workspace .env files, so even a correct-looking variable in the wrong file will be stripped.
Credential variables are honoured — ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY — but endpoints are configuration, not environment. That is a deliberate design decision, and once you accept it the rest is simple.
Installing
The install script covers macOS, Linux and WSL2:
curl -fsSL https://openclaw.ai/install.sh | bash
On Windows, iwr -useb https://openclaw.ai/install.ps1 | iex. If you would rather manage it yourself, npm install -g openclaw@latest works on a current Node runtime, and Docker is supported.
The configuration file
Configuration lives at ~/.openclaw/openclaw.json and is parsed as JSON5 — so comments, trailing commas and unquoted keys are all legal, which is why examples in the documentation look like relaxed JavaScript rather than strict JSON. You can point elsewhere with OPENCLAW_CONFIG_PATH.
Secrets interpolate with ${VAR_NAME}, restricted to uppercase names. Use $${VAR} if you need a literal. Put the actual values in the global dotenv at ~/.openclaw/.env, which takes precedence over workspace files and is not subject to the key blocking described above.
Declaring a custom provider
This is the block that works. Declare a new provider ID with an explicit model list:
{
models: {
mode: "merge",
providers: {
cozy: {
baseUrl: "https://api.cozyapi.com/v1",
apiKey: "${COZY_API_KEY}",
api: "openai-completions",
timeoutSeconds: 300,
models: [
{
id: "kimi-k3",
name: "Kimi K3",
reasoning: false,
input: ["text"],
contextWindow: 1000000,
maxTokens: 32000,
},
{
id: "glm-5.2",
name: "GLM 5.2",
input: ["text"],
contextWindow: 1000000,
maxTokens: 32000,
},
],
},
},
},
agents: {
defaults: {
model: { primary: "cozy/kimi-k3" },
},
},
}
With COZY_API_KEY=cozy_your_key_here in ~/.openclaw/.env.
Four fields carry the weight. baseUrl ends at /v1. api: "openai-completions" selects the wire format — this is the default for a custom provider with a base URL, and it is worth stating explicitly so the file documents itself. Each id must match exactly what your endpoint expects, because it is sent verbatim. And agents.defaults.model.primary uses the provider/model form built from your provider key.
mode: "merge" keeps the built-in providers alongside yours; "replace" drops them. Merge is the default and usually what you want, since it leaves you a fallback.
Declare a new provider, do not override a built-in
The tempting shortcut is to set baseUrl on the existing openai or anthropic provider and keep its model list. The documentation suggests this works for the OpenAI provider, and there are reports of the equivalent silently failing for Anthropic — built-in models continuing to ignore the custom base URL, with no traffic reaching the proxy and no error to explain why.
Declaring your own provider ID with an explicit models array avoids the ambiguity entirely. It is more typing and it behaves predictably, which is the correct trade for infrastructure configuration.
Both API shapes are supported
The api field is not limited to the OpenAI chat shape. Documented values include openai-completions, openai-responses, anthropic-messages, google-generative-ai, google-vertex, github-copilot, bedrock-converse-stream, ollama and Azure variants.
That matters if your gateway exposes more than one shape. You can declare two provider entries against the same host — one openai-completions, one anthropic-messages — and compare behaviour without changing anything else.
Other per-provider fields worth knowing: headers for a gateway that wants something beyond a bearer token, authHeader, contextWindow and maxTokens as provider-level defaults, and timeoutSeconds, which is worth raising from the default for long agentic turns.
The model policy gotcha
This one is genuinely easy to get wrong. agents.defaults.models looks like it should be an allowlist. It is not — it holds aliases and per-model settings, and does not restrict what can be selected.
The actual allowlist is agents.defaults.modelPolicy.allow:
openclaw config set agents.defaults.modelPolicy.allow \
'["cozy/*"]' --strict-json
If you want to guarantee that nothing escapes to a built-in provider — which is the usual reason for running behind a single gateway in the first place — this is the setting that enforces it. Configuring the wrong key produces a setup that appears locked down and is not.
Verifying it works
The CLI has commands for exactly this:
openclaw models status
openclaw models list
openclaw models set cozy/kimi-k3
models status is the fastest way to confirm your provider was parsed and authenticated. models list shows what the gateway believes is available, which catches the common failure where a JSON5 syntax error caused your provider block to be skipped without complaint.
In a chat session, /model cozy/glm-5.2 switches model, with -s to scope the change to the session rather than persisting it.
There is also a non-interactive onboarding path that accepts a custom base URL, API key and model ID as flags, which is the cleanest route if you are provisioning machines from a script rather than editing files by hand.
Troubleshooting
- No traffic reaching your gateway — you are relying on an environment variable, or overriding a built-in provider. Declare a new provider ID.
- Provider missing from
models list— a JSON5 parse error earlier in the file. The block is skipped rather than reported. - 401 — the
${VAR}did not resolve. Check the variable is uppercase and lives in~/.openclaw/.env, not a workspace file. - 404 — a doubled
/v1, or a modelidthat does not match what the endpoint serves. - Timeouts on long tasks — raise
timeoutSecondson the provider. - Requests going somewhere unexpected — set
modelPolicy.allow, notmodels.
The short version
One provider block in ~/.openclaw/openclaw.json with baseUrl, apiKey, api: "openai-completions" and an explicit model list. Credentials in ~/.openclaw/.env. Default model set through agents.defaults.model.primary. Allowlist through modelPolicy.allow. Verify with openclaw models status, and do not spend time on environment variables that the gateway was designed to ignore.
Common questions
Why does OPENAI_BASE_URL not work with OpenClaw?
OpenClaw treats endpoints as configuration rather than environment. It does not read base URL variables, and it strips keys ending in _BASE_URL or _ENDPOINT from workspace .env files. Declare a provider in openclaw.json instead.
Should I override the built-in openai provider or add a new one?
Add a new one with an explicit models array. Overriding a built-in provider behaves inconsistently — there are reports of custom base URLs being silently ignored while the gateway keeps using the default endpoint.
How do I restrict OpenClaw to only my own gateway?
Set agents.defaults.modelPolicy.allow. The similarly named agents.defaults.models key holds aliases and per-model settings and does not restrict anything, which makes it easy to configure a lockdown that is not actually in effect.