# POST /v1/responses

OpenAI's Responses API. Point an OpenAI SDK at `https://demo-api.routerplus.com/v1` and call `client.responses.create` with your RouterPlus key. Codex (the CLI, the desktop app and the IDE extension), the OpenAI Agents SDK and other Responses API tools call this route.

OpenAI's models are a passthrough. Claude and the other chat models are a **preview**: the gateway translates them through its chat surface. The chat endpoints are [POST /v1/chat/completions](/docs/api-chat-completions) and [POST /v1/messages](/docs/api-messages).

```
POST https://demo-api.routerplus.com/v1/responses
POST https://demo-api.routerplus.com/v1/responses/compact
```

## Examples

```bash
curl -N https://demo-api.routerplus.com/v1/responses \
  -H "Authorization: Bearer $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"gpt-6-luna","stream":true,"store":false,"max_output_tokens":200,"input":"Say hello in one sentence."}'
```

Python:

```python
import os
from openai import OpenAI

client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"])

resp = client.responses.create(
    model="gpt-6-luna",
    store=False,
    max_output_tokens=200,
    input="Say hello in one sentence.",
)
print(resp.output_text)
print(resp.usage)  # input, cached, output and reasoning tokens, and cost (USD)
```

TypeScript:

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://demo-api.routerplus.com/v1",
  apiKey: process.env.TM_API_KEY,
});

const stream = await client.responses.create({
  model: "gpt-5.6-terra",
  store: false,
  stream: true,
  input: "Count to five.",
});

for await (const event of stream) {
  if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
  if (event.type === "response.completed") console.log("\n", event.response.usage);
}
```

The examples use the official `openai` SDK with only the base URL changed. To set up Codex, see [Coding agents](/docs/install#codex).

## Which models

| Models | Status | How the gateway serves them |
|---|---|---|
| `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-sol`, `gpt-6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-4o`, `gpt-4o-mini`, `gpt-4.1-mini` | Supported | Passthrough to OpenAI's own Responses API, on the marketplace route or on your own OpenAI connection. The body goes as you sent it, except for the fields in [Request](#request). The answer and its stream events come back as OpenAI sent them, with `usage.cost` added. |
| Claude models (`claude-sonnet-5-5`, `claude-opus-5-5`, …, and their `rp-` names) and the other chat models | Preview | Translated to a chat request and served with every chat rule. The answer is translated back. See [Preview: Claude and other models](#preview-claude-and-other-models). |

- A deployed model from [Optimize](/docs/model-search) (`tm/<name>-v<n>`) and a [dedicated endpoint](/docs/dedicated-endpoints) are not served here. They are a 400 with code `model_not_supported_on_responses`, whose message points to `/v1/chat/completions`.
- Image models answer on [POST /v1/images/generations](/docs/api-images) only, and decision models on [POST /v1/decisions](/docs/api-decisions) only.
- An id that is not in the catalog is a 404. The gateway never substitutes another model.

## Authentication

| Header | Format |
|---|---|
| `Authorization` | `Bearer tm_vk_...` (what the OpenAI SDK and Codex send) |
| `x-api-key` | `tm_vk_...` (also accepted, same key) |

A missing or invalid key returns 401 with `error_type: "auth"`. None of your request headers reach the provider. The gateway sends its own key, or your connection's.

## Request

The body is OpenAI's Responses API body, at most 10 MB. The gateway changes these fields:

| Field | Handling |
|---|---|
| `model` | A catalog id. The provider gets its own id for the model. |
| `store` | Always `false`. The gateway keeps no responses, and OpenAI does not keep the response for later retrieval. OpenAI's own retention policy still applies (see [Data policy](/docs/data-policy)). A `store: true` is dropped and named in `x-tm-dropped-params`. Leave `store` out or send `false`. |
| `previous_response_id`, `conversation` | Not supported: a 400 `invalid_request`. Send the whole conversation in `input` on every turn. To keep the model's reasoning between turns, ask for `include: ["reasoning.encrypted_content"]` and send the reasoning items back (Codex does both). |
| `background` | `true` is a 400 `invalid_request`. Stream long answers instead. |
| `max_output_tokens` | Optional. A value you send must be a whole number from 1 to the model's own maximum, or to 32,768 if the model's maximum is lower. OpenAI's Responses API takes a limit above GPT-4o's 16,384, and the model stops at its own maximum. When you send none, the gateway adds none: OpenAI uses the model's own maximum, and the balance hold counts at most 32,768 output tokens of it. A limit that you send is held in full, up to the model's own maximum. |
| `prompt_cache_key` | Your value, else your session id (`session_id`, or the `x-session-id`, `x-session-affinity` or `x-claude-code-session-id` header). |
| `safety_identifier`, `user` | On the marketplace route, one hash of your organization and your end-user id goes as `safety_identifier`, never your raw value (see [Sessions and prompt caching](/docs/compat#sessions-and-prompt-caching)). |
| `service_tier` | On the marketplace route, a tier other than `auto` or `default` is dropped and named, because the catalog prices the default tier. |
| `temperature`, `top_p` | Dropped and named on GPT-6 and GPT-5.6, which take only their default sampling (`1` passes). |
| `tools` | Every type passes: function, custom, namespace, and OpenAI's hosted `web_search`. On the marketplace route, the hosted tools `code_interpreter`, `image_generation`, `file_search` and `mcp`, and a stored `prompt` template, are a 400 that names them. They are billed outside the token price, or they read data kept in an OpenAI account. Your own OpenAI connection takes them all. |
| `provider` | The routing controls of [Routing policies](/docs/routing-policies). The gateway reads them and never forwards them. |

Every other field passes to OpenAI as you sent it: input items of every type (reasoning items with encrypted content, custom tool calls), `instructions`, `tool_choice`, `parallel_tool_calls`, `reasoning`, `text`, `include`, `metadata`, `max_tool_calls` and `truncation`.

**Web search** is OpenAI's hosted `web_search` tool, billed at OpenAI's price: $10 per 1,000 calls, plus the tokens of the search results. The older `web_search_preview` tool costs $25 per 1,000 calls on GPT-4o, GPT-4o mini and GPT-4.1 mini. The fee is part of `usage.cost`. See [Pricing & billing](/docs/pricing#web-search).

`POST /v1/responses/compact` passes to OpenAI the same way and is billed from its usage. It never streams. It takes only the fields OpenAI's compact endpoint takes, and the gateway drops and names the others.

`GET /v1/responses/{id}` and the other stored-response endpoints are a 404. The gateway does not store responses.

## Response

On the supported models, the response is OpenAI's `response` object as OpenAI sent it, plus `usage.cost`: the exact charge in USD (see [Pricing & billing](/docs/pricing)).

- Usage is OpenAI's: `input_tokens` (cache reads and cache writes included), `input_tokens_details.cached_tokens`, `input_tokens_details.cache_write_tokens`, `output_tokens` and `output_tokens_details.reasoning_tokens`. They are billed with the same math as every other route.
- The `x-request-id` response header gives the id for `GET https://demo-api.routerplus.com/v1/generation?id=<x-request-id>`.
- Every call appears in `GET https://demo-api.routerplus.com/v1/usage`.

With `stream: true`, the events are OpenAI's server-sent events, record for record, in OpenAI's order: `response.created`, `response.in_progress`, `response.output_item.added`, `response.output_text.delta`, …, and `response.completed` (or `response.incomplete`). Its `response.usage` has the billed usage and `cost`. This API sends no `[DONE]`.

- The first events wait until the provider sends output. If a provider fails before that, the next route replaces it and you do not see it.
- If the provider fails after output started, the stream ends with the gateway's own `response.failed` event (code `server_error`, the canonical class in `metadata.error_type`), and nothing follows it. Codex retries it.
- If you close the stream early, you pay for the input and the output streamed to you so far, as on the chat surfaces (see [Pricing & billing](/docs/pricing#reserve-then-settle)).

The other response headers are the same as on [POST /v1/chat/completions](/docs/api-chat-completions#response-headers): `x-request-id`, `x-tm-provider`, `x-tm-attempts`, `x-tm-dropped-params` and the rest.

## Preview: Claude and other models

For a model that OpenAI does not serve (Claude, by its id or its `rp-` name, and the other chat models), the gateway translates the request into a chat request. It serves it with the chat surface's rules: routing and failover, the output default and its hold, automatic caching on Claude, billing and the ledger. The answer comes back as a `response` object or as Responses stream events. Codex works this way with Claude models.

| Responses input | Chat form |
|---|---|
| `instructions` and `developer` messages | System messages |
| Message items with `input_text`, `output_text`, `input_image`, `input_file` and `input_audio` parts | Chat messages with `text`, `image_url`, `file` and `input_audio` parts |
| `function_call` and `function_call_output` items | An assistant tool call and its tool message. A tool output with an image (Codex's `view_image`) goes into a user message right after the tool messages |
| `custom` tools, `custom_tool_call` and its output | A function tool with one string parameter, `input`. The call comes back as a `custom_tool_call` |
| `namespace` tools | Function tools named `<namespace>__<name>` (with a short hash when that name is taken or too long). The call comes back under its own namespace and name |
| `max_output_tokens`, `reasoning.effort`, `text.format` | `max_tokens`, `reasoning_effort`, `response_format` |
| `tool_choice`, `parallel_tool_calls`, `prompt_cache_key`, `temperature`, `top_p` | Their chat forms, or as sent |
| Hosted tools (`web_search`, `file_search`, `code_interpreter`, `image_generation`, `computer_use_preview`, `mcp`, `shell`, `apply_patch` and others), and `reasoning` items you send back | Dropped and named in `x-tm-dropped-params`, for example `tools[3].web_search` and `input[*].reasoning`. Web search is not mapped to Claude's own search |

- **Also dropped and named:** `reasoning.summary` (Codex asks for one on every request), a `text.verbosity` other than `medium`, `store: true`, and a `truncation`, `max_tool_calls`, `service_tier` or `top_logprobs` value that asks for something.
- **Refused with a 400 that names the field:** `previous_response_id`, `conversation`, `prompt`, `background: true`, `item_reference` items, an uploaded `file_id`, a `file_url`, and a part or item type that a chat body cannot carry.
- With `"provider": {"require_parameters": true}`, a drop is a 400 too.

The answer:

- Its `model` is the name you sent. Its `id` is `resp_` and the request id. The `x-request-id` header has the request id for `GET /v1/generation`.
- Text comes back as a `message` item, a tool call as a `function_call` or `custom_tool_call` item, and the model's reasoning as a `reasoning` item with its text.
- `status` is `incomplete` when the answer reached `max_output_tokens` or was filtered, and `completed` otherwise.
- Usage: `input_tokens` counts cache reads and writes, as on the chat surface, and `cost` is inside it.

Limits of the preview:

- Claude routes refuse `text.format` with a 400 that names it. `reasoning.effort` reaches Claude as its own effort (`output_config.effort`, see [Wire compatibility](/docs/compat)), so Codex's `model_reasoning_effort` works on Claude.
- The Claude 5 models refuse a forced tool choice (`tool_choice: "required"`, or a named tool) with a 400. Codex sends `auto`.
- A reasoning item that you send back is dropped, so the model's reasoning does not carry from turn to turn. The exception is Morph's and Mistral's routes: a reasoning item with the model's reasoning text, as this surface returns it, goes back to them as that turn's reasoning.
- Annotations and logprobs are not carried. A custom tool's input arrives whole when its call ends.
- `POST /v1/responses/compact` takes OpenAI's models only. For any other model it is a 400 with code `model_not_supported_on_responses`.

For production traffic to Claude, use `/v1/chat/completions` or `/v1/messages`.

## Errors

Every error uses OpenAI's error envelope, with the canonical class in `error.metadata.error_type`, as on [POST /v1/chat/completions](/docs/api-chat-completions#errors). A provider's refusal has the provider's own words in the message.

- On OpenAI's models, `error.code` is the canonical class, as on the chat surface. A prompt too long for the model has `code: "context_length_exceeded"`.
- On the preview path, `error.code` uses OpenAI's own names where clients act on them: `rate_limit_exceeded` (class `rate_limit`), `context_length_exceeded` (`context_overflow`), `model_not_found` (`model_unavailable`) and `invalid_prompt` (`content_policy`).
- On the preview path, a failure in the middle of a stream is a `response.failed` event. Any other code there is `server_error`, which Codex retries.
- A 400 for a field that the translation wrote names your field: `reasoning.effort`, `text.format` or `max_output_tokens`.

Codex compacts a conversation when it sees `context_length_exceeded`. The full table is on [Errors](/docs/errors).

See also: [POST /v1/chat/completions](/docs/api-chat-completions) · [Errors](/docs/errors) · [Wire compatibility](/docs/compat) · [Coding agents](/docs/install)
