OpenAI-Compatible Chat Completions Endpoint on Gateway
Learn how to expose an OpenAI-compatible /v1/chat/completions endpoint from your Gateway. This page covers enabling the feature, available routes, and important security considerations for operators.
Read this when
- Integrating tools that expect OpenAI Chat Completions
The Gateway can expose a minimal OpenAI-compatible Chat Completions interface. By default, this feature is turned off.
When activated, the following endpoints become available on the same port the Gateway already uses for WS and HTTP multiplexing:
| Method | Path |
|---|---|
| POST | /v1/chat/completions |
| GET | /v1/models |
| GET | /v1/models/{id} |
| POST | /v1/embeddings |
| POST | /v1/responses |
Each request executes as a standard Gateway agent run, following the identical code path as openclaw agent. Because of this, routing, permissions, and configuration all behave exactly as they do elsewhere in your Gateway setup.
Enabling the endpoint
{
gateway: {
http: {
endpoints: {
chatCompletions: { enabled: true },
},
},
},
}
To turn this off, set enabled: false or simply leave it unset.
Security boundary (important)
This endpoint should be viewed as full operator access to the gateway instance:
- A valid Gateway token or password accepted here carries the same weight as an owner or operator credential, not a limited per-user scope.
- The control-plane agent path used by trusted operator actions is the same one these requests travel, so any sensitive tools permitted by the target agent's policy are reachable through this endpoint.
- Restrict exposure to loopback, tailnet, or private ingress only. Never make it reachable from the public internet.
Authentication behavior is summarized below:
| Auth path | Behavior |
|---|---|
gateway.auth.mode="token" or "password" + Authorization: Bearer ... | Demonstrates possession of the shared gateway secret. Any x-openclaw-scopes header is disregarded, and the complete default operator scope set is restored: operator.admin, operator.approvals, operator.pairing, operator.read, operator.talk.secrets, operator.write. Chat turns are treated as originating from an owner sender. |
Trusted identity-bearing HTTP (trusted-proxy auth, or gateway.auth.mode="none" on private ingress) | When x-openclaw-scopes is supplied, it is respected; without it, the default operator scope set applies. Owner semantics are lost only if the caller explicitly narrows scopes and leaves out operator.admin. For owner-level operations such as x-openclaw-model, operator.admin is mandatory. |
Refer to Operator scopes, Security, and Remote access for more detail.
Authentication
Authentication relies on the Gateway auth configuration, with mode-specific details covered in Trusted proxy auth:
| Mode | How to authenticate |
|---|---|
gateway.auth.mode="token" | Authorization: Bearer <token>. Configure it through gateway.auth.token or OPENCLAW_GATEWAY_TOKEN. |
gateway.auth.mode="password" | Authorization: Bearer <password>. Configure it through gateway.auth.password or OPENCLAW_GATEWAY_PASSWORD. |
gateway.auth.mode="trusted-proxy" | Send traffic through the configured identity-aware proxy, which adds the required identity headers. For same-host loopback proxies, gateway.auth.trustedProxy.allowLoopback = true must be explicitly enabled. |
gateway.auth.mode="none" | No auth header is needed, but this works only on private ingress. |
Additional points:
- Callers on the same host that skip the proxy on a
trusted-proxygateway can authenticate directly withgateway.auth.passwordorOPENCLAW_GATEWAY_PASSWORD. However, if anyForwarded,X-Forwarded-*, orX-Real-IPheader is present, the request is kept on the trusted-proxy path. - When
gateway.auth.rateLimitis set and repeated auth failures occur, the endpoint responds with429and includes aRetry-Afterheader.
When to use this endpoint
- Choose this over creating a new built-in channel when your integration is simply another operator or client surface for the same gateway.
- For native mobile clients that connect directly to a remote gateway, use WebChat or the Gateway Protocol with the paired-device bootstrap or device-token flow instead, so the device never needs a shared HTTP token or password.
- If you are integrating an external messaging network that has its own users, rooms, webhook delivery, or outbound transport, build a channel plugin. See Building plugins.
Agent-first model contract
OpenClaw interprets the OpenAI model field as an agent target, not as a raw provider model identifier.
model value | Routes to |
|---|---|
openclaw | The agent configured as the default |
openclaw/default | The configured default agent (stable alias; safe to hardcode even if the actual default agent id shifts between environments) |
openclaw/<agentId> or openclaw:<agentId> | A particular agent |
agent:<agentId> | A particular agent (compatibility alias) |
Optional request headers:
| Header | Effect |
|---|---|
x-openclaw-model: <provider/model-or-bare-id> | Swaps the backend model for the chosen agent. Shared-secret bearer callers can apply it directly; identity-bearing callers (trusted-proxy, or private no-auth ingress with x-openclaw-scopes) must use operator.admin, otherwise 403 missing scope: operator.admin is enforced. |
x-openclaw-agent-id: <agentId> | Compatibility override for picking the agent. |
x-openclaw-session-key: <sessionKey> | Explicit session routing. Returns 400 invalid_request_error if a reserved internal namespace (subagent:, cron:, acp:) is used. |
x-openclaw-message-channel: <channel> | Assigns the synthetic ingress channel context for channel-aware prompts/policies. |
/v1/models enumerates top-level agent targets (openclaw, openclaw/default, openclaw/<agentId>), not backend provider models and not sub-agents; sub-agents remain internal execution topology. Omitting x-openclaw-model means the selected agent runs with its normal configured model.
/v1/embeddings relies on the same agent-target model ids. Send x-openclaw-model (from a shared-secret caller, or an identity-bearing caller with operator.admin) to target a specific embedding model; otherwise the request uses the selected agent's standard embedding setup.
Session behavior
By default the endpoint is stateless per request (a fresh session key is generated for every call).
When the request carries an OpenAI user string, the Gateway derives a stable session key from it so repeated calls can share an agent session. For custom apps, reuse the same user value per conversation thread; avoid account-level identifiers unless you want multiple conversations/devices to share one OpenClaw session. Use x-openclaw-session-key only when you need explicit routing control across multiple clients/threads, with application-owned keys that avoid the reserved namespaces above.
Request limits
The endpoint enforces built-in limits of 20 MB per request body, 8 image_url
parts from the latest user message, and 20 MB of cumulative decoded image
data. Image source policy remains configurable under
gateway.http.endpoints.chatCompletions.images:
{
gateway: {
http: {
endpoints: {
chatCompletions: {
enabled: true,
images: {
allowUrl: false,
urlAllowlist: ["cdn.example.com", "*.assets.example.com"],
allowedMimes: [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/heic",
"image/heif",
],
maxBytes: 10485760,
maxRedirects: 3,
timeoutMs: 10000,
},
},
},
},
},
}
Image settings default to:
| Key | Default |
|---|---|
images.allowUrl | false (URL-sourced image_url parts are rejected unless enabled) |
images.maxBytes | 10MB per image |
images.maxRedirects | 3 |
images.timeoutMs | 10s |
HEIC/HEIF image_url sources are accepted and normalized to JPEG before provider delivery through the shared OpenClaw image processor (Rastermill), which falls back to a system converter (sips, ImageMagick, GraphicsMagick, or ffmpeg) for formats needing external codec support.
Security note: allowlisting a hostname does not bypass private/internal IP blocking. For internet-exposed gateways, apply network egress controls in addition to app-level guards. See Security.
Chat tool contract
/v1/chat/completions supports a function-tool subset compatible with common OpenAI Chat clients.
Supported request fields
| Field | Notes |
|---|---|
tools | A collection of { "type": "function", "function": { ... } } |
tool_choice | One of "auto", "none", "required", or { "type": "function", "function": { "name": "..." } } |
messages[*].role: "tool" | Subsequent exchanges in the conversation |
messages[*].tool_call_id | Attaches the output of a tool back to its originating tool invocation |
max_completion_tokens | Positive safe integer; limits total completion tokens (including reasoning tokens) per call. This is the current field name, and it takes effect when both fields are present. If null or absent, no cap is applied. |
max_tokens | Positive safe integer; older name kept for compatibility. It is still checked when max_completion_tokens is set, but then disregarded for precedence. If null or absent, no cap is applied. |
temperature | Value from 0 to 2; sent to the upstream provider on a best-effort basis. Returns 400 invalid_request_error when outside this range. |
top_p | Value from 0 to 1; best-effort. Returns 400 invalid_request_error when outside this range. |
frequency_penalty | Value from -2.0 to 2.0; best-effort. Returns 400 invalid_request_error when outside this range. |
presence_penalty | Value from -2.0 to 2.0; best-effort. Returns 400 invalid_request_error when outside this range. |
seed | Whole number; best-effort. Returns 400 invalid_request_error when a non-integer is supplied. |
stop | A single string or an array of up to 4 strings; best-effort. Returns 400 invalid_request_error when more than 4 sequences are given or any entry is not a string or is empty. |
Every sampling and token-cap field travels through the same agent stream-param channel and is relayed on a best-effort basis:
- Token cap: the transport decides the wire field name, using
max_completion_tokensfor OpenAI-style endpoints andmax_tokensfor providers that recognize only the older name (Mistral, Chutes). stopis translated into the transport's stop field:stopfor Chat Completions backends,stop_sequencesfor Anthropic. Since the OpenAI Responses API lacks a stop parameter,stopis never applied to models running on Responses.- The ChatGPT-based Codex Responses backend relies on fixed server-side sampling and removes
temperature/top_p(together withmax_output_tokens,metadata,prompt_cache_retention,service_tier) before the request reaches that backend.
Unsupported variants
Returns 400 invalid_request_error when any of these conditions hold:
toolsis not an array, a tool entry is not a function, ortool.function.nameis absenttool_choiceforms likeallowed_toolsandcustomtool_choice.function.namevalues that fail to match any provided tool
For tool_choice: "required" and function-pinned tool_choice, the endpoint limits which client function tools are exposed, tells the runtime to invoke a client tool before replying, and raises an error if the agent's response lacks a matching structured client-tool call. This restriction applies to the caller-supplied HTTP tools list, not to every internal OpenClaw agent tool.
Non-streaming tool response shape
When tools are invoked by the agent, the response contains:
choices[0].finish_reason = "tool_calls"choices[0].message.tool_calls[]entries carryingid,type: "function",function.name,function.arguments(JSON string)- Assistant text preceding the tool call, placed in
choices[0].message.content(may be empty)
Streaming tool response shape
When stream: true, tool calls are delivered as incremental SSE chunks: an initial delta for the assistant role, optional deltas for assistant commentary, one or more delta.tool_calls chunks that convey tool identity and argument fragments, and finally a chunk containing finish_reason: "tool_calls" and data: [DONE].
If stream_options.include_usage=true is set, a final usage chunk gets emitted just before [DONE] appears.
Tool follow-up loop
Once tool_calls arrives, run the requested functions, then issue another request that carries the assistant's earlier tool-call message along with one or more role: "tool" messages whose tool_call_id values line up with those calls. That keeps the same agent reasoning loop going until the final answer is produced.
Streaming (SSE)
To enable Server-Sent Events, configure stream: true as follows:
Content-Type: text/event-stream- Every event line comes through as
data: <json> - The stream concludes with
data: [DONE]
Open WebUI quick setup
- Base URL:
http://127.0.0.1:18789/v1 - Docker on macOS base URL:
http://host.docker.internal:18789/v1 - API key: your Gateway bearer token
- Model:
openclaw/default
What to expect: GET /v1/models returns openclaw/default, which Open WebUI adopts as the chat model id. To target a particular backend provider or model, either set the agent's usual default model or pass x-openclaw-model (for a shared-secret caller, or an identity-bearing caller that has operator.admin).
Quick smoke test:
curl -sS http://127.0.0.1:18789/v1/models \
-H 'Authorization: Bearer YOUR_TOKEN'
If the response is openclaw/default, most Open WebUI setups should work using that same base URL and token.
Examples
To keep one app conversation stable:
curl -sS http://127.0.0.1:18789/v1/chat/completions \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"model": "openclaw/default",
"user": "conv:YOUR_CONVERSATION_ID",
"messages": [{"role":"user","content":"Summarize my tasks for today"}]
}'
On later calls for that conversation, reuse the same user value so the agent session continues uninterrupted.
Non-streaming:
curl -sS http://127.0.0.1:18789/v1/chat/completions \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"model": "openclaw/default",
"messages": [{"role":"user","content":"hi"}]
}'
Streaming:
curl -N http://127.0.0.1:18789/v1/chat/completions \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'x-openclaw-model: openai/gpt-5.4' \
-d '{
"model": "openclaw/research",
"stream": true,
"messages": [{"role":"user","content":"hi"}]
}'
List models:
curl -sS http://127.0.0.1:18789/v1/models \
-H 'Authorization: Bearer YOUR_TOKEN'
Fetch one model:
curl -sS http://127.0.0.1:18789/v1/models/openclaw%2Fdefault \
-H 'Authorization: Bearer YOUR_TOKEN'
Create embeddings:
curl -sS http://127.0.0.1:18789/v1/embeddings \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'x-openclaw-model: openai/text-embedding-3-small' \
-d '{
"model": "openclaw/default",
"input": ["alpha", "beta"]
}'
/v1/embeddings accepts input either as a single string or as an array of strings.