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:

MethodPath
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 pathBehavior
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:

ModeHow 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-proxy gateway can authenticate directly with gateway.auth.password or OPENCLAW_GATEWAY_PASSWORD. However, if any Forwarded, X-Forwarded-*, or X-Real-IP header is present, the request is kept on the trusted-proxy path.
  • When gateway.auth.rateLimit is set and repeated auth failures occur, the endpoint responds with 429 and includes a Retry-After header.

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 valueRoutes to
openclawThe agent configured as the default
openclaw/defaultThe 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:

HeaderEffect
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:

KeyDefault
images.allowUrlfalse (URL-sourced image_url parts are rejected unless enabled)
images.maxBytes10MB per image
images.maxRedirects3
images.timeoutMs10s

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

FieldNotes
toolsA collection of { "type": "function", "function": { ... } }
tool_choiceOne of "auto", "none", "required", or { "type": "function", "function": { "name": "..." } }
messages[*].role: "tool"Subsequent exchanges in the conversation
messages[*].tool_call_idAttaches the output of a tool back to its originating tool invocation
max_completion_tokensPositive 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_tokensPositive 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.
temperatureValue from 0 to 2; sent to the upstream provider on a best-effort basis. Returns 400 invalid_request_error when outside this range.
top_pValue from 0 to 1; best-effort. Returns 400 invalid_request_error when outside this range.
frequency_penaltyValue from -2.0 to 2.0; best-effort. Returns 400 invalid_request_error when outside this range.
presence_penaltyValue from -2.0 to 2.0; best-effort. Returns 400 invalid_request_error when outside this range.
seedWhole number; best-effort. Returns 400 invalid_request_error when a non-integer is supplied.
stopA 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_tokens for OpenAI-style endpoints and max_tokens for providers that recognize only the older name (Mistral, Chutes).
  • stop is translated into the transport's stop field: stop for Chat Completions backends, stop_sequences for Anthropic. Since the OpenAI Responses API lacks a stop parameter, stop is 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 with max_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:

  • tools is not an array, a tool entry is not a function, or tool.function.name is absent
  • tool_choice forms like allowed_tools and custom
  • tool_choice.function.name values 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 carrying id, 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.

2,105 words · updated Aug 3, 2026