# POST /v1/responses

OpenAI's Responses API. Point an OpenAI SDK at `https://api.routerplus.com/v1` and call `client.responses.create` with your marketplace key. Codex (the CLI, the desktop app and the IDE extension), the OpenAI Agents SDK and other tools built on the Responses API call this route. OpenAI's models are served as a passthrough. Claude and the other chat models are a **preview** here: the gateway translates them through its chat surface. The chat twins of this route are [POST /v1/chat/completions](/docs/api-chat-completions) and [POST /v1/messages](/docs/api-messages).

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

## 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), and 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 | Served through the chat surface, with every chat rule: the request is translated to a chat request, and the answer 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 an honest 404, never a silent substitute.

## 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, capped at 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; when you send one, it 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: 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); read by the gateway, never forwarded. |

Everything else 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** on the Responses API is OpenAI's hosted `web_search` tool, billed at OpenAI's price: $10 per 1,000 calls, in addition to 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` is passed to OpenAI the same way and billed from its usage. It never streams, and it takes only the fields OpenAI's compact endpoint takes; the gateway drops and names the others. `GET /v1/responses/{id}` and the other stored-response endpoints are a 404: stored-response retrieval is unsupported.

## Response

On the supported models the response is OpenAI's `response` object, as OpenAI sent it, with one addition: `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`, billed with the same math as every other route. The `x-request-id` response header gives the id for `GET https://api.routerplus.com/v1/generation?id=<x-request-id>`, and every call appears in `GET https://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`), whose `response.usage` carries the billed usage and `cost`. There is no `[DONE]` on this API.

- The first events wait until the provider sends output, so a provider that fails before that is replaced by the next route without you seeing 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 and serves it with the chat surface's own 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 carries 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 `reasoning.effort` and `text.format` with a 400 that names them. In Codex, do not set `model_reasoning_effort` for a Claude model.
- 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 is not carried from turn to turn. Morph's and Mistral's routes are the exception: a reasoning item with the model's reasoning text, as this surface answers it, goes back to them as that turn's reasoning and is not dropped.
- Annotations and logprobs are not carried, and 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`.

Use `/v1/chat/completions` or `/v1/messages` for production traffic to Claude.

## 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 carries 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` takes 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`). A failure in the middle of a stream is a `response.failed` event, and 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).

## Examples

```bash
curl -N https://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 — the official `openai` SDK with the base URL swapped:

```python
import os
from openai import OpenAI

client = OpenAI(base_url="https://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 — the official `openai` package, streamed:

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://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);
}
```

To set up Codex, see [Coding agents](/docs/install#codex).

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