POST /v1/responses
The OpenAI Responses wire — what Codex CLI speaks, and what the OpenAI SDKs' client.responses calls. Base URL: https://sator-api.princep.org/v1. Every model in the catalog is served on it.
Stateless, by design#
The Responses API is two things: a request and response shape, and a store of past responses on OpenAI's servers that a later turn can point at by id. Sator serves the shape and keeps no store — nothing you send is written down between requests (Privacy). So a request is accepted exactly when it stands on its own:
- Send the whole conversation in
inputon every turn. This is what Codex CLI does (store: false, full history), what the OpenAI SDKs do unless you opt into the store, and what LangChain does. previous_response_idandconversationare refused,400with codeunsupported_parameternaming the field. So is aninputitem of typeitem_reference, andbackground: true.storeis alwaysfalse. Sendingtrueis not an error; it is simply not what happens, and the response says"store": false.
Request#
curl https://sator-api.princep.org/v1/responses \
-H "Authorization: Bearer $SATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"instructions": "Be brief.",
"input": [
{"role": "user", "content": "Say hi"}
],
"max_output_tokens": 64
}'| Field | Notes |
|---|---|
model | Required. An id from Models. Unknown → 404 model_not_found. |
input | Required. A string, or an array of items: messages (user, assistant, system, developer), function_call, function_call_output, reasoning. |
instructions | The system prompt. |
max_output_tokens | Optional. A value above the model's max output is lowered to it. |
stream | true for server-sent events. See below and Streaming. |
temperature, top_p | Sampling, passed through. |
tools, tool_choice, parallel_tool_calls | Function tools, in the Responses shape (type, name, description, parameters, strict at the top level). See Tool calling. |
text.format | text, json_object or json_schema. |
reasoning.effort | Passed through to models that take it. |
reasoning.summary | Ask for one (auto, concise, detailed) to receive a reasoning output item with readable summary text; without it no reasoning text is returned. |
prompt_cache_key | Your own conversation key, up to 256 characters. See Prompt caching. |
include, text.verbosity, user, safety_identifier, metadata | Passed through to models that take them. |
Content parts: input_text, input_image with a base64 data URL (a remote image URL is not fetched), and input_file with file_data. Bodies over 5 MB are rejected 413.
A namespace tool — how Codex CLI groups its function tools — is accepted everywhere: on a model that is not Responses-native its member functions stand in for the group, and calls come back with the bare function name.
Built-in tools — web_search, file_search, computer_use, code_interpreter, mcp, custom grammars — are OpenAI's own and are not available on any model here. On a model that is not Responses-native they are refused 400 invalid_body with param: "tools"; on a Responses-native model they are passed through and the model's own provider answers for them. Codex CLI sends web_search by default; its page says how to turn that off.
Response#
{
"id": "resp_...",
"object": "response",
"created_at": 1787593841,
"status": "completed",
"error": null,
"incomplete_details": null,
"model": "deepseek-v4-flash",
"output": [
{
"id": "msg_...",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [{"type": "output_text", "text": "Hi! How can I help you today?", "annotations": []}]
}
],
"parallel_tool_calls": true,
"store": false,
"usage": {
"input_tokens": 12,
"input_tokens_details": {"cached_tokens": 0},
"output_tokens": 9,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 21
}
}output is the list of items the model produced, in order: a reasoning item when you asked for a summary, a message, and one function_call item per tool call (call_id, name, arguments as a JSON string). status is incomplete with incomplete_details.reason: "max_output_tokens" when the output cap cut the answer short.
usage.input_tokens_details.cached_tokens is how many input tokens were served from cache and billed at the cache-read rate. Every response carries an x-request-id header; quote it to support.
Streaming#
With "stream": true the body is text/event-stream of named events, each with a sequence_number. Every output item is bracketed by response.output_item.added and response.output_item.done — the done carries the whole item — with deltas in between, and the stream ends at response.completed (or response.incomplete), whose response.usage carries the counts.
event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_...","status":"in_progress",...}}
event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_...","type":"message","status":"in_progress","role":"assistant","content":[]}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_...","output_index":0,"content_index":0,"delta":"Hi! How can I help"}
event: response.output_item.done
data: {"type":"response.output_item.done","sequence_number":7,"output_index":0,"item":{"id":"msg_...","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi! How can I help you today?","annotations":[]}]}}
event: response.completed
data: {"type":"response.completed","sequence_number":8,"response":{"id":"resp_...","status":"completed","output":[...],"usage":{"input_tokens":12,"output_tokens":9,...}}}Function-call arguments stream as response.function_call_arguments.delta; reasoning summaries as response.reasoning_summary_text.delta. A stream that fails mid-way ends with response.failed. See Streaming for disconnects and billing.
Which models are native#
gpt-5.6-luna, grok-4.6 and muse-spark-1.2 speak this wire natively and pass straight through — every event exactly as their provider emits it, reasoning items with encrypted_content included. Every other model is reached by translating the request onto the model's own wire and translating its answer back into the events above; the fields in the table are what survives the translation.
Errors#
The OpenAI envelope, {"error": {"message", "type", "param", "code"}}. Every status and code is in Errors.