--- summary: "Expose an OpenAI-compatible /v1/chat/completions HTTP endpoint from the Gateway" read_when: - Integrating tools that expect OpenAI Chat Completions title: "OpenAI chat completions" --- The Gateway can serve a small OpenAI-compatible Chat Completions surface. It is **disabled by default**. Once enabled, it serves all of these on the same port as the Gateway (WS + HTTP multiplex): | Method | Path | | ------ | ---------------------- | | POST | `/v1/chat/completions` | | GET | `/v1/models` | | GET | `/v1/models/{id}` | | POST | `/v1/embeddings` | | POST | `/v1/responses` | Requests run as a normal Gateway agent run (same codepath as `openclaw agent`), so routing, permissions, and config match your Gateway. ## Enabling the endpoint ```json5 { gateway: { http: { endpoints: { chatCompletions: { enabled: true }, }, }, }, } ``` Set `enabled: false` (or omit it) to disable. ## Security boundary (important) Treat this endpoint as **full operator access** to the gateway instance: - A valid Gateway token/password for this endpoint is equivalent to an owner/operator credential, not a narrow per-user scope. - Requests run through the same control-plane agent path as trusted operator actions, so if the target agent's policy allows sensitive tools, this endpoint can use them. - Keep it on loopback/tailnet/private ingress only. Do not expose it to the public internet. Auth matrix: | Auth path | Behavior | | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `gateway.auth.mode="token"` or `"password"` + `Authorization: Bearer ...` | Proves possession of the shared gateway secret. Ignores any `x-openclaw-scopes` header and restores the full default operator scope set: `operator.admin`, `operator.approvals`, `operator.pairing`, `operator.read`, `operator.talk.secrets`, `operator.write`. Treats chat turns as owner-sender turns. | | Trusted identity-bearing HTTP (trusted-proxy auth, or `gateway.auth.mode="none"` on private ingress) | Honors `x-openclaw-scopes` when present; falls back to the default operator scope set when absent. Loses owner semantics only when the caller explicitly narrows scopes and omits `operator.admin`. Requires `operator.admin` for owner-level controls such as `x-openclaw-model`. | See [Operator scopes](/gateway/operator-scopes), [Security](/gateway/security), and [Remote access](/gateway/remote). ## Authentication Uses the Gateway auth configuration (see [Trusted proxy auth](/gateway/trusted-proxy-auth) for that mode's details): | Mode | How to authenticate | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `gateway.auth.mode="token"` | `Authorization: Bearer `. Set via `gateway.auth.token` or `OPENCLAW_GATEWAY_TOKEN`. | | `gateway.auth.mode="password"` | `Authorization: Bearer `. Set via `gateway.auth.password` or `OPENCLAW_GATEWAY_PASSWORD`. | | `gateway.auth.mode="trusted-proxy"` | Route through the configured identity-aware proxy; it injects the required identity headers. Same-host loopback proxies need explicit `gateway.auth.trustedProxy.allowLoopback = true`. | | `gateway.auth.mode="none"` | No auth header required (private ingress only). | Notes: - Same-host callers that bypass the proxy on a `trusted-proxy` gateway can fall back to `gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD` directly. Any `Forwarded`, `X-Forwarded-*`, or `X-Real-IP` header evidence keeps the request on the trusted-proxy path instead. - If `gateway.auth.rateLimit` is configured and too many auth attempts fail, the endpoint returns `429` with a `Retry-After` header. ## When to use this endpoint - Prefer this over adding a new built-in channel when your integration is just another operator/client surface for the same gateway. - For native mobile clients that connect directly to a remote gateway, prefer [WebChat](/web/webchat) or the [Gateway Protocol](/gateway/protocol) with the paired-device bootstrap/device-token flow, so the device does not need a shared HTTP token/password. - Build a channel plugin instead when integrating an external messaging network with its own users, rooms, webhook delivery, or outbound transport. See [Building plugins](/plugins/building-plugins). ## Agent-first model contract OpenClaw treats the OpenAI `model` field as an **agent target**, not a raw provider model id. | `model` value | Routes to | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `openclaw` | Configured default agent | | `openclaw/default` | Configured default agent (stable alias; safe to hardcode even if the real default agent id changes between environments) | | `openclaw/` or `openclaw:` | Specific agent | | `agent:` | Specific agent (compatibility alias) | Optional request headers: | Header | Effect | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x-openclaw-model: ` | Overrides the backend model for the selected agent. Shared-secret bearer callers can use this directly; identity-bearing callers (trusted-proxy, or private no-auth ingress with `x-openclaw-scopes`) need `operator.admin`, otherwise `403 missing scope: operator.admin`. | | `x-openclaw-agent-id: ` | Compatibility override for agent selection. | | `x-openclaw-session-key: ` | Explicit session routing. Rejected with `400 invalid_request_error` if it uses a reserved internal namespace (`subagent:`, `cron:`, `acp:`). | | `x-openclaw-message-channel: ` | Sets the synthetic ingress channel context for channel-aware prompts/policies. | `/v1/models` lists top-level agent targets (`openclaw`, `openclaw/default`, `openclaw/`), not backend provider models and not sub-agents; sub-agents stay internal execution topology. If you omit `x-openclaw-model`, the selected agent runs with its normal configured model. `/v1/embeddings` uses the same agent-target `model` ids. Send `x-openclaw-model` (from a shared-secret caller, or an identity-bearing caller with `operator.admin`) to pick a specific embedding model; otherwise the request uses the selected agent's normal embedding setup. ## Session behavior By default the endpoint is **stateless per request** (a new session key is generated each call). If the request includes 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 (config) Defaults can be tuned under `gateway.http.endpoints.chatCompletions`: ```json5 { gateway: { http: { endpoints: { chatCompletions: { enabled: true, maxBodyBytes: 20000000, maxImageParts: 8, maxTotalImageBytes: 20000000, 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, }, }, }, }, }, } ``` Defaults when omitted: | Key | Default | | --------------------- | --------------------------------------------------------------------------- | | `maxBodyBytes` | 20MB | | `maxImageParts` | 8 (max `image_url` parts read from the latest user message) | | `maxTotalImageBytes` | 20MB (cumulative decoded bytes across all `image_url` parts in one request) | | `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](/gateway/security). ## Chat tool contract `/v1/chat/completions` supports a function-tool subset compatible with common OpenAI Chat clients. ### Supported request fields | Field | Notes | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `tools` | Array of `{ "type": "function", "function": { ... } }` | | `tool_choice` | `"auto"`, `"none"`, `"required"`, or `{ "type": "function", "function": { "name": "..." } }` | | `messages[*].role: "tool"` | Follow-up turns | | `messages[*].tool_call_id` | Binds a tool result back to a prior tool call | | `max_completion_tokens` | Number; per-call cap on total completion tokens (reasoning tokens included). Current field name; used when both it and `max_tokens` are sent. | | `max_tokens` | Number; legacy alias, ignored when `max_completion_tokens` is also present. | | `temperature` | Number 0-2; best-effort, forwarded to the upstream provider. `400 invalid_request_error` if out of range. | | `top_p` | Number 0-1; best-effort. `400 invalid_request_error` if out of range. | | `frequency_penalty` | Number -2.0 to 2.0; best-effort. `400 invalid_request_error` if out of range. | | `presence_penalty` | Number -2.0 to 2.0; best-effort. `400 invalid_request_error` if out of range. | | `seed` | Integer; best-effort. `400 invalid_request_error` for non-integer values. | | `stop` | String or array of up to 4 strings; best-effort. `400 invalid_request_error` for more than 4 sequences or non-string/empty entries. | All sampling and token-cap fields ride the same agent stream-param channel and are forwarded best-effort: - Token cap: the wire field name is chosen by the provider transport: `max_completion_tokens` for OpenAI-family endpoints, `max_tokens` for providers that only accept the legacy name (Mistral, Chutes). - `stop` maps to the transport's stop field: `stop` for Chat Completions backends, `stop_sequences` for Anthropic. The OpenAI Responses API has no stop parameter, so `stop` is not applied on Responses-backed models. - The ChatGPT-based Codex Responses backend uses fixed server-side sampling and strips `temperature`/`top_p` (along with `max_output_tokens`, `metadata`, `prompt_cache_retention`, `service_tier`) before the request reaches that backend. ### Unsupported variants Returns `400 invalid_request_error` for: - non-array `tools`, non-function tool entries, or missing `tool.function.name` - `tool_choice` variants such as `allowed_tools` and `custom` - `tool_choice.function.name` values that do not match a provided tool For `tool_choice: "required"` and function-pinned `tool_choice`, the endpoint narrows the exposed client function-tool set, instructs the runtime to call a client tool before responding, and errors if the agent response has no matching structured client-tool call. This applies to the caller-supplied HTTP `tools` list, not every internal OpenClaw agent tool. ### Non-streaming tool response shape When the agent calls tools, the response uses: - `choices[0].finish_reason = "tool_calls"` - `choices[0].message.tool_calls[]` entries with `id`, `type: "function"`, `function.name`, `function.arguments` (JSON string) - Assistant commentary before the tool call, in `choices[0].message.content` (possibly empty) ### Streaming tool response shape When `stream: true`, tool calls arrive as incremental SSE chunks: an initial assistant role delta, optional assistant commentary deltas, one or more `delta.tool_calls` chunks carrying tool identity and argument fragments, then a final chunk with `finish_reason: "tool_calls"` and `data: [DONE]`. If `stream_options.include_usage=true`, a trailing usage chunk is emitted before `[DONE]`. ### Tool follow-up loop After receiving `tool_calls`, execute the requested function(s) and send a follow-up request that includes the prior assistant tool-call message plus one or more `role: "tool"` messages with matching `tool_call_id`. This continues the same agent reasoning loop to produce the final answer. ## Streaming (SSE) Set `stream: true` to receive Server-Sent Events: - `Content-Type: text/event-stream` - Each event line is `data: ` - Stream ends 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` Expected behavior: `GET /v1/models` lists `openclaw/default`, and Open WebUI uses it as the chat model id. For a specific backend provider/model, set the agent's normal default model, or send `x-openclaw-model` (shared-secret caller, or identity-bearing caller with `operator.admin`). Quick smoke test: ```bash curl -sS http://127.0.0.1:18789/v1/models \ -H 'Authorization: Bearer YOUR_TOKEN' ``` If that returns `openclaw/default`, most Open WebUI setups can connect with the same base URL and token. ## Examples Stable session for one app conversation: ```bash 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"}] }' ``` Reuse the same `user` value on later calls for that conversation to continue the same agent session. Non-streaming: ```bash 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: ```bash 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: ```bash curl -sS http://127.0.0.1:18789/v1/models \ -H 'Authorization: Bearer YOUR_TOKEN' ``` Fetch one model: ```bash curl -sS http://127.0.0.1:18789/v1/models/openclaw%2Fdefault \ -H 'Authorization: Bearer YOUR_TOKEN' ``` Create embeddings: ```bash 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` supports `input` as a string or array of strings. ## Related - [Configuration reference](/gateway/configuration-reference) - [Operator scopes](/gateway/operator-scopes) - [OpenAI](/providers/openai)