Skip to main content

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, at the same prices and limits, with the same API keys.

Stateless by design​

Serenity Edge keeps no prompts and no answers (see 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:

ParameterAcceptedRefused
storefalse or omittedtrue
previous_response_idomitted or nullany id
conversationomitted or nullany value
backgroundfalse or omittedtrue
prompt (stored prompt templates)omitted or nullany 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 for its configuration.

A first request​

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)
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:

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.
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. 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:

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 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:

[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:

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 on chat. The SDK's parse helper works:

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).

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​

ParameterNotes
modelAny chat model id or alias of Models.
inputString or array of input items (see above). Required.
instructionsSystem message for this request.
max_output_tokensPositive integer. It is capped at the model's output limit, as max_tokens is on chat.
temperature, top_pAs on chat. Without reasoning, the gateway fills in the model's recommended values for any you omit.
tools, tool_choice, parallel_tool_callsFunction tools, alone or grouped in namespace tools.
text.formattext, json_object or json_schema. text.verbosity is accepted and has no effect.
reasoning.effortnone (default), minimal, low, medium, high or xhigh. reasoning.summary is accepted, and no summary is produced.
streamtrue for events.
includereasoning.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_callsAccepted; they do not change the answer.
client_metadataAn object. Accepted and dropped: it reaches neither the model nor the response.
truncationdisabled 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, in the same {"error": {...}} envelope, with the Responses field names in param (input, max_output_tokens). The ones specific to this route:

StatusMessage (start)Cause
400This API is stateless (zero data retention): responses are never stored, so 'store': true is not supported.A stateful parameter (see above). param names it.
400Only 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".
400Input items of type 'web_search_call' are not supportedAn item of a hosted tool in input.
400Images must be sent inline as base64 data URLsAn image by URL or file id.
400Unknown parameter: 'messages'.A field the Responses API does not have, such as chat's messages.
404This 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/.