OpenClaw OpenAI-Compatible API Guide: /v1/chat/completions vs /v1/responses, and Why model Names an Agent
Use /v1/chat/completions when you have an existing OpenAI SDK client or a UI like Open WebUI that already resends the whole conversation, and use /v1/responses when you want server-side continuation through previous_response_id or need to send files. Both ship disabled, both run a full OpenClaw agent turn, and in both the model field picks an agent (openclaw/default), not a provider model.
You pasted http://127.0.0.1:18789/v1 into Open WebUI, typed gpt-5 in the model box out of habit, and got an error. Or worse, it worked, you asked a follow-up, and the agent had no idea what you were talking about. Both of those are the endpoint behaving exactly as documented.
I worked from the official OpenAI chat completions and OpenResponses API pages as published on November 5, 2026. Config keys, defaults, limits and status codes below come from those two pages. Where they disagree with each other, or leave a gap, I say so.
Turn it on first
Nothing answers on /v1 until you opt in, and the two endpoints have separate switches:
gateway: {
http: {
endpoints: {
chatCompletions: { enabled: true }, // POST /v1/chat/completions
responses: { enabled: true }, // POST /v1/responses
},
},
}Either switch also brings up GET /v1/models, GET /v1/models/{id} and POST /v1/embeddings on the same port as the Gateway's WebSocket. Leave the one you don't use off. That contrast with /tools/invoke, which is always on, is covered in the tools invoke guide.
Four HTTP doors into one Gateway
| Endpoint | Default | Conversation state | Files and images | Best for | Key trade-off |
|---|---|---|---|---|---|
POST /v1/chat/completions | Off | Stateless; user or x-openclaw-session-key makes it sticky | Up to 8 image_url parts; remote URLs off by default | Existing OpenAI SDK code, Open WebUI | No file inputs, and a full agent turn per call |
POST /v1/responses | Off | Stateless; previous_response_id continues for up to 30 days | Images (10 MB) and files (5 MB, PDF capped at 4 pages); URLs on by default | New code, document Q&A, multi-step tool loops | Unknown or expired response ids return 400, so you need a resend path |
POST /tools/invoke | Always on | None, one tool call | Whatever the tool takes, 2 MB body | Scripts that already know the tool | No model, so no judgment |
POST /hooks/agent | Off | Per hook session | JSON payload | Third-party events (CI, alerts) | Separate hook token, safety-wrapped, but fire-and-forget by default |
The hooks row has its own page, the webhooks guide. This one is about the first two.
The model field picks an agent
Every request runs through the same agent path as openclaw agent. Routing, tool policy and the agent's configured model all apply. So model can't name gpt-5 or a Claude model id. It names the agent:
model value | Routes to |
|---|---|
openclaw | Default agent |
openclaw/default | Default agent (stable alias, the one I use) |
openclaw/<agentId> or openclaw:<agentId> | That agent |
agent:<agentId> | That agent (compatibility form) |
GET /v1/models lists top-level agents only. No provider models, no sub-agents. Listing needs operator.read.
To swap the backend model for one request, send x-openclaw-model. A shared-secret caller can use it freely. An identity-bearing caller without operator.admin gets a 403. I treat that header as a debugging tool and nothing more, because a client that silently overrides the agent's model also bypasses whatever failover chain you tuned for that agent, and the bill shows up somewhere you weren't watching.
Why your follow-up forgot everything
Each call gets a fresh session key by default. That is fine for Open WebUI, which resends the full message history on every turn anyway. It is not fine for a script that sends only the newest message and expects the agent to remember.
Three ways to keep a conversation:
- The OpenAI
userstring. Sameuser, same derived session key, on both endpoints. x-openclaw-session-keyfor keys your app owns. Thesubagent:,cron:andacp:prefixes are reserved and return 400.previous_response_id, on/v1/responsesonly. It reuses the prior session when the auth subject, agent id and session key header all match.
The user option has a sharp edge the docs warn about in one line. If you pass your customer's account id as user, every teammate on that account lands in one session and reads each other's context. Use something per person, or per conversation.
Response-id continuation is stored in the core:openresponses keyed store, for up to 30 days and only the newest 5,000 responses. An id that fell off returns a 400, even with stream: true. The fix is to resend full history without the id, so keep that history client-side somewhere. Incognito responses never create a mapping at all.
Archived sessions answer with 409 (or an error event mid-stream). Unarchive, then retry. The sessions CLI guide shows how.
A Gateway token here is an owner credential
Same rule as /tools/invoke, same scope list. With gateway.auth.mode set to token or password, the bearer header grants operator.admin, operator.approvals, operator.pairing, operator.read, operator.talk.secrets and operator.write, ignores any narrower x-openclaw-scopes, and counts every chat turn as an owner-sender turn. The agent can then use any tool its policy allows.
That is the part people miss when they hand the token to a front end. Open WebUI with the Gateway token in its settings is a web page with owner access to your agent, and every person who can log into that Open WebUI inherits it. I run it on the Gateway host behind its own login, or on a tailnet (the Tailscale guide covers that), and point it at an agent with a tight tool allowlist instead of the default one.
Narrower scopes only work in trusted-proxy mode or none on private ingress. A same-host proxy also needs gateway.auth.trustedProxy.allowLoopback = true. With gateway.auth.rateLimit set, repeated failures get a 429 with Retry-After.
The smallest working call
curl -sS http://127.0.0.1:18789/v1/chat/completions \
-H 'Authorization: Bearer <gateway-token>' \
-H 'Content-Type: application/json' \
-d '{
"model": "openclaw/default",
"user": "alex-laptop",
"messages": [{ "role": "user", "content": "What is on my calendar today?" }]
}'From the OpenAI Python SDK it is two lines of setup: base_url="http://127.0.0.1:18789/v1" and api_key set to the Gateway token. For Open WebUI in Docker on macOS, the base URL is http://host.docker.internal:18789/v1. Smoke test with GET /v1/models first. If openclaw/default isn't in the list, nothing else will work either.
Request fields that behave differently than OpenAI's
Chat completions accepts tools (function type only), tool_choice, max_completion_tokens (preferred over legacy max_tokens), temperature 0 to 2, top_p 0 to 1, the two penalties from -2 to 2, seed, and up to 4 stop strings. Out-of-range values get a 400.
Sampling is best-effort. The docs name two cases: stop is ignored when the backend is the Responses API, and the Codex backend ignores temperature and top_p. A 200 does not mean your temperature: 0 was honored.
On /v1/responses, max_tool_calls, reasoning, metadata, store and truncation are accepted and ignored. Truncation by output budget shows up as status: "incomplete" with incomplete_details.reason: "max_output_tokens", the counterpart of finish_reason: "length".
Client tools, and the empty string you have to send
The tools you pass are client-side functions. OpenClaw returns finish_reason: "tool_calls" with JSON-string arguments, your code runs the function, and you send back the assistant message plus a tool message with the matching tool_call_id. If the function returned nothing, send "content": "". Leaving content out does not complete the call.
tool_choice: "required" or a pinned function narrows only the tools you supplied, and errors if the agent doesn't call one. On /v1/responses that error is a 502 (or response.failed mid-stream). The chat completions page says it errors and does not name the status code. Handle both.
Images and files, where the two pages disagree
This is the difference I would have gotten wrong without reading both pages side by side. Chat completions ships images.allowUrl as false. Responses ships images.allowUrl and files.allowUrl as true. Same Gateway, same token, and one endpoint fetches remote URLs from a request body while the other refuses to.
Allowlists (urlAllowlist, wildcards supported) never bypass private-IP blocking, but that is the only egress guard you get. If the Gateway can reach the internet and you enable responses, I would set a urlAllowlist on day one, or flip allowUrl off and send base64.
Limits worth knowing: a 20 MB body, a 60-second body read (slower uploads get 408), 8 URL parts, 10 MB per image with HEIC and HEIF converted to JPEG. Responses files cap at 5 MB, 60,000 extracted characters, and 4 PDF pages. Extracted file text goes into the system prompt as untrusted external content, not into session history, so a follow-up turn won't see the file unless you send it again.
Streaming and cancellation
stream: true returns server-sent events ending in data: [DONE]. With stream_options.include_usage, the usage chunk appears only when usage is complete. When it isn't, JSON responses omit usage rather than reporting zeros.
Closing the connection cancels the agent run. Pending URL downloads die with it.
That matters more than it sounds. A reverse proxy with a 30-second idle timeout in front of a slow agent turn will quietly kill runs that would have finished, and the agent never gets to the end of what it was doing. If the Gateway itself cancels while you are connected, you get a 500 api_error reading "agent run was cancelled", not a provider timeout. Check the Gateway log before blaming the model.
Status codes
| Code | Meaning | What I check first |
|---|---|---|
| 400 | Bad field, reserved session prefix, or an unknown previous_response_id | Session key prefix, then whether the response id expired |
| 401 | Gateway auth failed, or the provider has no credentials (authentication_error) | Read the message: it says which of the two |
| 403 | x-openclaw-model or incognito continuation without operator.admin | Auth mode and scopes |
| 404 | Unknown provider model | The x-openclaw-model value |
| 408 | Body not read within 60 s | Large base64 images over a slow link |
| 409 | Session archived | Unarchive it |
| 429 | Rate limited, with Retry-After | A client retrying a stale token |
| 500 | Unexpected failure or a Gateway-side cancel, credentials redacted | Gateway log |
| 502 | Responses: required tool choice produced no matching call | Tool descriptions the agent can actually act on |
| 503 | Provider overloaded | Retry with backoff |
Embeddings, briefly
POST /v1/embeddings takes a string or an array, reports real token usage or zeros (never an estimate), and accepts dimensions to override memory.search.outputDimensionality. If the backend returns a different vector size, you get a 400 with embedding_dimensions_mismatch. One odd failure mode: if provider cleanup fails, embeddings for that agent and provider stay blocked until a Gateway restart.
Where each endpoint fits (and where it does not)
/v1/chat/completions fits Open WebUI and any OpenAI SDK code you don't want to rewrite. It is the weaker choice for documents, since it takes no file inputs, and for scripts that send only the latest message.
/v1/responses wins for new code: previous_response_id saves you resending history, files work, and the event names say what happened. It loses if you can't tolerate the 400 on an expired id, or if you forgot that it fetches URLs by default.
/tools/invoke beats both when the script already knows which tool to call. Paying a model turn to run sessions_list is waste.
Neither is the right answer for a native mobile app talking to a remote Gateway. The docs point you to WebChat or the Gateway Protocol with device pairing, and I agree, because pairing gives each device its own revocable identity instead of a copy of the owner token.
Final verdict
Enable only the endpoint you use. Send openclaw/default as the model, pick a user value per person rather than per account, and set a urlAllowlist before you turn on responses. Then go look at who can log into the Open WebUI holding your Gateway token, because every one of them is an owner now.
Ready to build?
Get the OpenClaw Starter Kit — config templates, 5 production-ready skills, deployment checklist. Go from zero to running in under an hour.
$14 $6.99
Get the Starter Kit →Also in the OpenClaw store
Get the free OpenClaw quickstart guide
Step-by-step setup. Plain English. No jargon.