The Gateway can serve an OpenResponses-compatible POST /v1/responses endpoint. It is disabled by default and shares its port with the Gateway (WS + HTTP multiplex): http://<gateway-host>:<port>/v1/responses.

Requests run as a normal Gateway agent run (same codepath as apus agent), so routing, permissions, and config match your Gateway.

Enable or disable with gateway.http.endpoints.responses.enabled. When enabled, the same compatibility surface also serves GET /v1/models, GET /v1/models/{id}, and POST /v1/embeddings.

POST /v1/chat/completions is enabled separately with gateway.http.endpoints.chatCompletions.enabled. See OpenAI Chat Completions.

Authentication, security, and routing

Operational behavior matches OpenAI Chat Completions:

For the canonical explanation of agent-target models, apus/default, embeddings pass-through, and backend model overrides, see OpenAI Chat Completions.

See Operator scopes and Security.

Session behavior

By default the endpoint is stateless per request (a new session key is generated each call).

If the request includes an OpenResponses user string, the Gateway derives a stable session key from it so repeated calls can share an agent session.

previous_response_id reuses the earlier response's session when the request stays within the same agent/user/requested-session scope (matched by auth subject, agent id, and x-apus-session-key).

Continuation mappings survive Gateway restarts in the shared state database's core keyed store (core:openresponses, namespace response-sessions) for up to 30 days, capped at the newest 5,000 responses across the Gateway. This matches the default session maintenance age and count; it does not extend the lifetime of the underlying session or transcript. Mappings contain only response/session identifiers, the auth subject (an installation-keyed bearer HMAC or verified proxy identity), agent and requested-session scope, and store-managed timestamps. No response content is copied, and no separate table or schema migration is required. The keyed store rejects expired mappings immediately on lookup and removes expired rows on subsequent writes and through the shared plugin-state maintenance sweep, which runs once per minute in bounded batches. If continuity persistence fails after an otherwise successful run, the endpoint returns HTTP 500 or a streaming response.failed, rather than reporting a success whose response ID cannot be continued.

An unknown, expired, evicted, or out-of-scope previous_response_id returns the same HTTP 400 with invalid_request_error, including when stream: true. To recover, resend the full input history and omit previous_response_id; the Gateway never silently starts a new conversation for an unresolved continuation. Responses issued before this storage change cannot be recovered after the old Gateway exits.

Incognito responses never create continuation mappings. Continue them explicitly with the same x-apus-session-key while the Incognito session is alive; using their response ID returns the same 400 as an unknown ID.

Explicit incognito session continuation

Explicitly selecting or continuing an incognito conversation with x-apus-session-key (the sessionKey override) requires effective operator.admin authority. This rule follows authority, not ingress: it denies both trusted-proxy callers without owner/admin authority and private gateway.auth.mode="none" callers that explicitly narrow x-apus-scopes below admin (for example, to operator.write). Either receives HTTP 403 with a forbidden error. A profile-less private no-auth caller on this path gets missing scope: operator.admin; for a profile-backed caller, the response hides the private target with this error shape (where <sessionKey> is the requested override):

{
  "error": {
    "message": "Incognito session \"<sessionKey>\" was not found.",
    "type": "forbidden"
  }
}

Owner/admin callers keep explicit incognito session continuation. A private no-auth request without x-apus-scopes receives the default operator scopes, including operator.admin, and is therefore treated as owner/admin. Reserved internal namespace overrides (subagent:, cron:, acp:) remain a separate validation failure and still return HTTP 400 with invalid_request_error.

Request shape

Field Support
input String or array of item objects.
instructions Merged into the system prompt.
tools Client tool definitions (function tools).
tool_choice "auto", "none", "required", or { "type": "function", "name": "..." } to filter or require client tools.
stream Enables SSE streaming.
max_output_tokens Best-effort output limit (provider dependent).
temperature Best-effort sampling temperature. Ignored by the ChatGPT-based Codex Responses backend, which uses fixed server-side sampling.
top_p Best-effort nucleus sampling. Same Codex Responses caveat as temperature.
user Stable session routing.
previous_response_id Session continuity (see above).
max_tool_calls, reasoning, metadata, store, truncation Accepted but currently ignored.

Items (input)

message

Roles: system, developer, user, assistant.

function_call_output (turn-based tools)

Send tool results back to the model:

{
  "type": "function_call_output",
  "call_id": "call_123",
  "output": "{\"temperature\": \"72F\"}"
}

reasoning and item_reference

Accepted for schema compatibility but ignored when building the prompt.

Tools (client-side function tools)

Provide tools with tools: [{ type: "function", name, description?, parameters? }].

If the agent calls a tool, the response returns a function_call output item. Send a follow-up request with function_call_output to continue the turn.

If a tool produces no text, return output: "" with its call_id. The empty result still completes that tool call and can be the only new input item in a continuation.

Clients that manage their own history can append response.output to input, then append new user messages or function_call_output items. Keep returned assistant metadata and function-call IDs, names, and arguments unchanged. Alternatively, supply previous_response_id and only the new input items.

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 rejects the turn if it does not include a matching structured client-tool call, matching the /v1/chat/completions contract. Non-streaming requests return 502 with an api_error; streaming requests emit a response.failed event.

Images (input_image)

Supports base64 or URL sources:

{
  "type": "input_image",
  "source": { "type": "url", "url": "https://example.com/image.png" }
}

Allowed MIME types (default): image/jpeg, image/png, image/gif, image/webp, image/heic, image/heif. Max size (default): 10MB.

Files (input_file)

Supports base64 or URL sources:

{
  "type": "input_file",
  "source": {
    "type": "base64",
    "media_type": "text/plain",
    "data": "SGVsbG8gV29ybGQh",
    "filename": "hello.txt"
  }
}

Allowed MIME types (default): text/plain, text/markdown, text/html, text/csv, application/json, application/pdf. Max size (default): 5MB.

Current behavior:

PDF parsing is provided by the bundled document-extract plugin, which uses clawpdf and its packaged PDFium WebAssembly runtime for text extraction and page rendering.

URL fetch defaults:

File + image limits

The endpoint uses a built-in 20 MB request-body limit. File and image source policy remains configurable under gateway.http.endpoints.responses:

{
  gateway: {
    http: {
      endpoints: {
        responses: {
          enabled: true,
          maxUrlParts: 8,
          files: {
            allowUrl: true,
            urlAllowlist: ["cdn.example.com", "*.assets.example.com"],
            allowedMimes: [
              "text/plain",
              "text/markdown",
              "text/html",
              "text/csv",
              "application/json",
              "application/pdf",
            ],
            maxBytes: 5242880,
            maxChars: 60000,
            maxRedirects: 3,
            timeoutMs: 10000,
            pdf: {
              maxPages: 4,
              maxPixels: 4000000,
              minTextChars: 200,
            },
          },
          images: {
            allowUrl: true,
            urlAllowlist: ["images.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
maxUrlParts 8
files.maxBytes 5MB
files.maxChars 60k
files.maxRedirects 3
files.timeoutMs 10s
files.pdf.maxPages 4
files.pdf.maxPixels 4,000,000
files.pdf.minTextChars 200
images.maxBytes 10MB
images.maxRedirects 3
images.timeoutMs 10s

HEIC/HEIF input_image sources are normalized to JPEG before provider delivery through the shared Apus image processor (Rastermill), which falls back to a system converter (sips, ImageMagick, GraphicsMagick, or ffmpeg) for formats needing external codec support.

Security note: URL allowlists are enforced before fetch and on redirect hops. 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.

Streaming (SSE)

Streaming preserves repeated content from separate assistant messages. If a correction cannot be represented by appending to text already sent, the stream emits response.failed rather than completing with inconsistent content.

Set stream: true to receive Server-Sent Events:

Event types currently emitted: response.created, response.in_progress, response.output_item.added, response.content_part.added, response.output_text.delta, response.output_text.done, response.content_part.done, response.output_item.done, response.completed, response.incomplete (on output-budget truncation), response.failed (on error).

Failed agent runs, including whole-agent timeouts, return a failed response. Streaming failures emit response.failed followed by [DONE]; partial content may already have reached the client. Timeout settings follow the agent loop.

A reply that ends because the agent reached its output-token budget is returned with status: "incomplete" and incomplete_details: { "reason": "max_output_tokens" }, and its final message item carries status: "incomplete". Streaming emits these fields on the terminal response.incomplete event, so clients dispatching by event type also observe the truncation. This mirrors the finish_reason: "length" projection on /v1/chat/completions.

Disconnecting the HTTP client cancels active source-URL downloads and the agent run. If cancellation happens while preparing input, the Gateway releases that download and does not start another input download or the agent run. This applies to both streaming and non-streaming requests. If the Gateway itself cancels the agent run while the client is still connected, the request returns a failed response (HTTP 500 when not streaming) with the message agent run was cancelled, not a provider timeout.

Usage

usage is populated when the underlying provider reports token counts. Apus normalizes common OpenAI-style aliases before those counters reach downstream status/session surfaces, including input_tokens / output_tokens and prompt_tokens / completion_tokens.

Errors

Errors use a JSON object like:

{ "error": { "message": "...", "type": "invalid_request_error" } }

Common cases: 400 invalid request body, 401 missing/invalid auth, 403 missing operator scope, 405 wrong method, 429 too many failed auth attempts (with Retry-After).

Examples

Non-streaming:

curl -sS http://127.0.0.1:18789/v1/responses \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'x-apus-agent-id: main' \
  -d '{
    "model": "apus",
    "input": "hi"
  }'

Streaming:

curl -N http://127.0.0.1:18789/v1/responses \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'x-apus-agent-id: main' \
  -d '{
    "model": "apus",
    "stream": true,
    "input": "hi"
  }'

Related