Files
openclaw/docs/gateway/openai-http-api.md
Peter Steinberger f7d7148cf0 docs: rewrite published docs grounded in current source (#100142)
Source-grounded rewrite of 529 published docs pages with per-unit information-loss verification: 1,713 factual corrections cited to src/**, generated surfaces regenerated, frontmatter titles preserved for i18n, release notes pages untouched. All docs gates green.

Closes #100141
2026-07-05 00:32:47 -04:00

317 lines
20 KiB
Markdown

---
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 <token>`. Set via `gateway.auth.token` or `OPENCLAW_GATEWAY_TOKEN`. |
| `gateway.auth.mode="password"` | `Authorization: Bearer <password>`. 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/<agentId>` or `openclaw:<agentId>` | Specific agent |
| `agent:<agentId>` | Specific agent (compatibility alias) |
Optional request headers:
| Header | Effect |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-openclaw-model: <provider/model-or-bare-id>` | 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: <agentId>` | Compatibility override for agent selection. |
| `x-openclaw-session-key: <sessionKey>` | Explicit session routing. Rejected with `400 invalid_request_error` if it uses a reserved internal namespace (`subagent:`, `cron:`, `acp:`). |
| `x-openclaw-message-channel: <channel>` | Sets the synthetic ingress channel context for channel-aware prompts/policies. |
`/v1/models` lists top-level agent targets (`openclaw`, `openclaw/default`, `openclaw/<agentId>`), 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: <json>`
- 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)