# Responses API

> Call every chat model with OpenAI's Responses API (POST /v1/responses): stateless, with streaming events, function tools, structured outputs, reasoning and images.

Source: https://docs.serenityedge.ai/docs/responses-api/

`POST /v1/responses` serves OpenAI's Responses API, the interface of `client.responses.create` in the OpenAI SDKs, the Agents SDK and Codex. It reaches the same chat models as [`POST /v1/chat/completions`](https://docs.serenityedge.ai/docs/api/edge/create-chat-completion/), at the same [prices and limits](https://docs.serenityedge.ai/docs/pricing-and-limits/), with the same API keys.

## Stateless by design

Serenity Edge keeps no prompts and no answers (see [Data residency](https://docs.serenityedge.ai/docs/data-residency/)), so the Responses API here stores nothing. Send the whole conversation in `input` on every request, as you would with chat completions. A request that asks for stored state is refused with HTTP 400 and a message that says so:

| Parameter | Accepted | Refused |
| --- | --- | --- |
| `store` | `false` or omitted | `true` |
| `previous_response_id` | omitted or `null` | any id |
| `conversation` | omitted or `null` | any value |
| `background` | `false` or omitted | `true` |
| `prompt` (stored prompt templates) | omitted or `null` | any value |
| `item_reference` input items | — | always |

Every response says `"store": false`. Its `id` is never stored either: `GET`, `DELETE` and `cancel` on `/v1/responses/{id}`, and `input_items`, `compact` and `input_tokens` under `/v1/responses/`, answer HTTP 404.

The OpenAI SDKs work as they are. Codex already sends `store: false`; see [Codex CLI](https://docs.serenityedge.ai/docs/responses-api/#codex-cli) for its configuration.

## A first request

```python
from openai import OpenAI

client = OpenAI(base_url="https://api.serenityedge.ai/v1", api_key="YOUR_SERENITY_EDGE_API_KEY")

response = client.responses.create(
    model="orion-plus",
    instructions="Answer in one sentence.",
    input="Why does EU data residency matter for a SaaS company?",
)
print(response.output_text)
```

```bash
curl https://api.serenityedge.ai/v1/responses \
  -H "Authorization: Bearer $SERENITY_EDGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "orion-plus",
    "instructions": "Answer in one sentence.",
    "input": "Why does EU data residency matter for a SaaS company?"
  }'
```

`instructions` becomes the system message. `input` is a string, or an array of items: messages with the role `user`, `assistant`, `system` or `developer`, plus the `function_call`, `function_call_output` and `reasoning` items of earlier turns. System and developer messages, wherever they appear in `input`, are added to the system message in order.

## Conversations

Pass the previous turns in `input`. The output items of a response can go back as they are:

```python
history = [{"role": "user", "content": "My name is Ada."}]
first = client.responses.create(model="orion-plus", input=history)

history += [item.model_dump(exclude_none=True) for item in first.output]
history.append({"role": "user", "content": "What is my name?"})
second = client.responses.create(model="orion-plus", input=history)
print(second.output_text)  # Ada
```

## Streaming

With `stream: true` the answer is a `text/event-stream` of Responses events:

-   **Start:** every stream begins with `response.created` and `response.in_progress`.
-   **Items:** each output item follows: `response.output_item.added`, its content part, the deltas (`response.output_text.delta`, `response.reasoning_text.delta`, `response.function_call_arguments.delta`), then the matching `done` events.
-   **End:** exactly one `response.completed`, `response.incomplete` or `response.failed`. It carries the full response with `usage`.
-   **Order:** every event has a `sequence_number`. There is no `[DONE]` line.

```python
with client.responses.stream(model="orion-plus", input="Count from 1 to 5.") as stream:
    for event in stream:
        if event.type == "response.output_text.delta":
            print(event.delta, end="", flush=True)
    final = stream.get_final_response()
```

A long prompt may be preceded by `: ping` comments while the model reads it, as on [chat streams](https://docs.serenityedge.ai/docs/streaming/). If the model fails after the stream has started, the stream ends with `response.failed` and an `error` object whose `code` is `server_error`, `rate_limit_exceeded` or `invalid_prompt`.

## Function tools

Declare functions in `tools`. Answer each `function_call` in the output with a `function_call_output` that has the same `call_id`:

```python
import json

tools = [{
    "type": "function",
    "name": "get_weather",
    "description": "Current temperature for a city",
    "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
        "additionalProperties": False,
    },
}]
history = [{"role": "user", "content": "What's the weather in Paris?"}]
response = client.responses.create(model="orion-plus", input=history, tools=tools)

history += [item.model_dump(exclude_none=True) for item in response.output]
for call in (item for item in response.output if item.type == "function_call"):
    city = json.loads(call.arguments)["city"]
    history.append({"type": "function_call_output", "call_id": call.call_id, "output": json.dumps({"temp_c": 18})})

final = client.responses.create(model="orion-plus", input=history, tools=tools)
print(final.output_text)
```

`tool_choice` takes `auto`, `none`, `required` or `{"type": "function", "name": "..."}`, and `parallel_tool_calls` turns parallel calls on or off. Only function tools are served. Hosted tools are refused with HTTP 400: `web_search`, `file_search`, `code_interpreter`, `computer_use`, `image_generation`, remote `mcp` and custom tools.

Function tools may also come grouped in a `namespace` tool, `{"type": "namespace", "name": "...", "description": "...", "tools": [...]}`, as Codex groups the tools of its plugins and MCP servers. The model sees each one as a function named after its namespace and name, and a call comes back as a `function_call` with its `name` and `namespace`. Send it back in `input` as you got it. Every tool in a namespace must be a function tool.

## Codex CLI

[Codex CLI](https://github.com/openai/codex) talks to Serenity Edge through this API. Declare Serenity Edge as a provider in `~/.codex/config.toml`, and put your key in the `SERENITY_EDGE_API_KEY` environment variable:

```toml
[model_providers.serenity-edge]
name = "Serenity Edge"
base_url = "https://api.serenityedge.ai/v1"
env_key = "SERENITY_EDGE_API_KEY"
wire_api = "responses"
```

Then put the model settings in a profile, `~/.codex/serenity-edge.config.toml`, so your default model and provider stay as they are:

```toml
model = "orion-pro"
model_provider = "serenity-edge"
model_reasoning_effort = "medium"
model_context_window = 262144
web_search = "disabled"
```

Start Codex with `codex --profile serenity-edge`. To make Serenity Edge your default instead, put these five lines at the top of `config.toml`.

-   **`web_search = "disabled"` is required.** Codex otherwise sends its hosted web search tool, which Serenity Edge does not serve, and every request is refused with HTTP 400.
-   **`model_context_window`.** Codex does not know Serenity Edge's models. At start-up it logs `failed to refresh available models`, because it expects its own format for the model list, and then `Model metadata for 'orion-pro' not found`, and uses generic settings. Both warnings are harmless. Setting `model_context_window` gives Codex the real context of the chat models (262,144 tokens), so it compacts the conversation at the right time.
-   **Plugins and MCP servers work.** Their tools reach the model as functions. Codex sends the description of every tool on every turn, and those descriptions are billed as input tokens: with many plugins enabled, a turn can start at tens of thousands of input tokens. Turn off the plugins you do not need.
-   **Codex sends `client_metadata`** (its own installation, session and turn ids) on every request. It is accepted and dropped: it reaches neither the model nor any log.

## Structured outputs

`text.format` takes `text`, `json_object` or `json_schema`, with the same guarantees as [`response_format`](https://docs.serenityedge.ai/docs/structured-outputs/) on chat. The SDK's `parse` helper works:

```python
from pydantic import BaseModel

class Event(BaseModel):
    name: str
    day: str
    participants: list[str]

response = client.responses.parse(
    model="orion-pro",
    input="Alice and Bob are going to a science fair on Friday.",
    text_format=Event,
)
print(response.output_parsed)
```

## Reasoning

Reasoning is off unless you ask for it, as on chat. Send `reasoning.effort`:

-   `none` turns it off;
-   any other value turns it on: `minimal`, `low`, `medium`, `high` or `xhigh`.

The model's reasoning comes back as a `reasoning` item before the answer, in plain text in `content`, so `encrypted_content` is `null`. `usage.output_tokens` includes the reasoning tokens, and `usage.output_tokens_details.reasoning_tokens` counts them when the model reports them. A `reasoning` item sent back in `input` is given to the model with the next assistant turn.

## Images

Add `input_image` parts to a user message. Images must be inline base64 data URLs (`data:image/png;base64,...`); image URLs and file ids are refused, and so are file inputs. The limits of chat apply: up to 8 images per request, depending on the model (see [Image input](https://docs.serenityedge.ai/docs/models/#image-input)).

```python
response = client.responses.create(
    model="orion-plus",
    input=[{"role": "user", "content": [
        {"type": "input_text", "text": "What does this receipt total?"},
        {"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo..."},
    ]}],
)
```

## Output and usage

A response has these parts:

-   `output`: the `reasoning`, `message` and `function_call` items, in that order;
-   `status`: `completed`, or `incomplete` with `incomplete_details.reason` set to `max_output_tokens` when the answer reached `max_output_tokens`;
-   `usage`: `input_tokens` (with `input_tokens_details.cached_tokens`), `output_tokens` and `total_tokens`.

Billing is the same as on chat: input, cached input and output tokens at the model's prices.

## Parameters

| Parameter | Notes |
| --- | --- |
| `model` | Any chat model id or alias of [Models](https://docs.serenityedge.ai/docs/models/). |
| `input` | String or array of input items (see above). Required. |
| `instructions` | System message for this request. |
| `max_output_tokens` | Positive integer. It is capped at the model's output limit, as `max_tokens` is on chat. |
| `temperature`, `top_p` | As on chat. Without reasoning, the gateway fills in the model's recommended values for any you omit. |
| `tools`, `tool_choice`, `parallel_tool_calls` | Function tools, alone or grouped in `namespace` tools. |
| `text.format` | `text`, `json_object` or `json_schema`. `text.verbosity` is accepted and has no effect. |
| `reasoning.effort` | `none` (default), `minimal`, `low`, `medium`, `high` or `xhigh`. `reasoning.summary` is accepted, and no summary is produced. |
| `stream` | `true` for events. |
| `include` | `reasoning.encrypted_content` and `message.input_image.image_url` are accepted and change nothing; `message.output_text.logprobs` is refused. |
| `metadata`, `user`, `safety_identifier`, `prompt_cache_key`, `prompt_cache_retention`, `service_tier`, `max_tool_calls` | Accepted; they do not change the answer. |
| `client_metadata` | An object. Accepted and dropped: it reaches neither the model nor the response. |
| `truncation` | `disabled` only: input longer than the context window is refused, never cut. |

Not supported, refused with HTTP 400:

-   the stateful parameters above;
-   `context_management` (server-side compaction);
-   `top_logprobs` above 0;
-   unknown parameters.

WebSocket mode is not served.

## Errors

Errors are those of [chat completions](https://docs.serenityedge.ai/docs/errors-and-rate-limits/), in the same `{"error": {...}}` envelope, with the Responses field names in `param` (`input`, `max_output_tokens`). The ones specific to this route:

| Status | Message (start) | Cause |
| --- | --- | --- |
| 400 | `This API is stateless (zero data retention): responses are never stored, so 'store': true is not supported.` | A stateful parameter (see above). `param` names it. |
| 400 | `Only function tools are supported; hosted tools (type 'web_search') are not.` | A hosted tool in `tools`. For a web search tool the message adds how to turn it off in Codex: `web_search = "disabled"`. |
| 400 | `Input items of type 'web_search_call' are not supported` | An item of a hosted tool in `input`. |
| 400 | `Images must be sent inline as base64 data URLs` | An image by URL or file id. |
| 400 | `Unknown parameter: 'messages'.` | A field the Responses API does not have, such as chat's `messages`. |
| 404 | `This API is stateless (zero data retention): responses are never stored, so there is nothing to retrieve, list, cancel, compact or delete.` | A route under `/v1/responses/`. |
