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:
| 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 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.createdandresponse.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 matchingdoneevents. - End: exactly one
response.completed,response.incompleteorresponse.failed. It carries the full response withusage. - 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 logsfailed to refresh available models, because it expects its own format for the model list, and thenModel metadata for 'orion-pro' not found, and uses generic settings. Both warnings are harmless. Settingmodel_context_windowgives 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:
noneturns it off;- any other value turns it on:
minimal,low,medium,highorxhigh.
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: thereasoning,messageandfunction_callitems, in that order;status:completed, orincompletewithincomplete_details.reasonset tomax_output_tokenswhen the answer reachedmax_output_tokens;usage:input_tokens(withinput_tokens_details.cached_tokens),output_tokensandtotal_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. |
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_logprobsabove 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:
| 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/. |