Responses
API reference for the OpenAI Responses-compatible endpoint
Creates a model response using OpenAI's Responses API shape — the item-based alternative to Chat Completions. Point an OpenAI SDK's baseURL at Sansa and call client.responses.create(...) with no other changes. Sansa auto-routes each request to the best underlying model ("sansa-auto") or calls a specific model directly — same behavior as Completions; see Models for details.
POST /v1/responses
Sansa's Responses endpoint is stateless: nothing is stored server-side between requests. Send the full conversation in input on every call — see Stateless model below.
See the code panel for request and response examples.
Body Parameters
input
Required. A string (one user turn) or an array of items representing the conversation so far. Empty input returns 400.
// Shorthand for a single user message.
input: string
// Or an array of items:
input: InputItem[]Item types you send:
type | Purpose | Key fields |
|---|---|---|
message (or omit type and set role) | One conversation turn | role, content |
function_call | Echo back a tool call from a prior response | call_id, name, arguments |
function_call_output | The result of a tool call, for the model to read | call_id, output |
reasoning | Echo back a prior reasoning item for continuity | summary, encrypted_content |
interface MessageItem {
type?: "message"; // optional when role is present
role: "system" | "developer" | "user" | "assistant" | "tool";
content: string | ContentPart[];
}
interface ContentPart {
type: "input_text" | "input_image" | "input_file";
// For "input_text".
text?: string;
// For "input_image". Accepts an https URL or a data URI.
// detail: "low" | "high" | "auto" | "original" (maps to "high").
image_url?: string | { url: string; detail?: string };
detail?: string;
// For "input_file". Only image data URIs are supported
// (no Files API) — non-image files return 400.
file_data?: string;
}developer is accepted as an alias for system. input_audio content parts return 400 — Sansa is text-only on Responses (see Completions for input_audio support on chat).
Function tool round trip and reasoning continuity are covered in their own sections below.
instructions
Optional. A developer/system message that applies to this request only. Prepended ahead of the flattened input.
model
Optional. Same behavior as Completions: "sansa-auto", null, or omitted routes automatically; a catalog model ID calls that model directly; an unknown ID returns 400 invalid_model.
stream
Default: false. If true, the response is delivered as named Server-Sent Events — see Streaming below. This is a different wire format from Chat Completions streaming.
temperature
Default: 1.0. Same semantics as Completions.
top_p
Optional. Same semantics as Completions.
max_output_tokens
Optional. Upper bound on generated tokens, including reasoning tokens. Equivalent to max_completion_tokens on Chat Completions.
tools
Optional. Function tools in the flat Responses shape:
interface FunctionTool {
type: "function";
name: string;
description?: string;
// A JSON Schema object.
parameters: object;
strict?: boolean; // accepted; forwarded where supported
}Also accepted in tools:
web_search/web_search_preview— a hosted tool Sansa executes for you. See Web search.shell,apply_patch,custom— client-executed tools that round-trip like function calls; see Client-executed tools.type: "namespace"wrapping a nestedtoolsarray — unwrapped automatically.- Other hosted tool types (
file_search,code_interpreter,computer_use,image_generation,mcp,tool_search) are accepted and ignored rather than rejected, so a mixed tool array from an existing agent config doesn't 400.
tool_choice
Default: "auto". Same values as Completions ("auto", "none", "required", { type: "function", name: "..." }). { type: "web_search" } maps to "auto".
parallel_tool_calls
Default: true. Same as Completions.
reasoning
Optional. Reasoning configuration.
{
// "none" through "xhigh". "max" is accepted as an alias for "xhigh".
effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
// Requests a visible reasoning summary. Sansa does not run a separate
// summarizer — when the model produced plaintext reasoning, it fills
// `summary`; otherwise the reasoning item's summary is empty.
summary?: "auto" | "concise" | "detailed";
}See Reasoning for what each effort level means. On sansa-auto, omitting reasoning entirely still defaults to effort: "high" before routing, same as Completions.
text
Optional. Structured output configuration — equivalent to response_format on Completions.
// Default. Unstructured text.
{ format: { type: "text" } }
// Guaranteed valid JSON, no schema enforcement.
{ format: { type: "json_object" } }
// Structured outputs conforming to a JSON Schema.
{
format: {
type: "json_schema",
name: "...",
strict: true,
schema: { ... }
}
}include
Optional. Array of strings controlling extra output data. Only "reasoning.encrypted_content" has an effect — see Reasoning.
metadata, session_id, prompt_cache_key, prompt_cache_retention
Same semantics as the equivalent Completions fields. session_id also pins sansa-auto routing for sticky provider-cache behavior across turns — see Smart Routing.
safety_identifier, user
Optional. Accepted for OpenAI SDK compatibility. Used as a fallback source for metadata.end_user_id when neither sansa.end_user_id nor metadata.end_user_id is set (checked in that order).
Accepted, no effect
The following are accepted for compatibility but don't change behavior:
| Field | Note |
|---|---|
truncation | "disabled" is the effective behavior regardless of value; Sansa already clamps via its own budget logic |
stream_options.include_obfuscation | Accepted; obfuscation payloads are never emitted |
text.verbosity | Accepted, ignored |
service_tier | Accepted, ignored; echoed back as null |
top_logprobs | Accepted, ignored |
max_tool_calls | Accepted as a no-op — this field counts built-in tool calls on OpenAI; Sansa's function tools are unlimited and client-executed |
prompt_cache_options | Accepted; only a 1h/3600s TTL has an effect |
Returns
A response object.
interface Response {
id: string; // "resp_..."
object: "response";
created_at: number; // unix timestamp
status: "completed" | "incomplete" | "failed";
error: { code: string; message: string } | null;
incomplete_details: { reason: "max_output_tokens" | "content_filter" } | null;
// The model that actually served the request.
model: string;
output: OutputItem[];
usage: {
input_tokens: number;
input_tokens_details: { cached_tokens: number };
output_tokens: number;
output_tokens_details: { reasoning_tokens: number };
total_tokens: number;
};
// Echoed request fields.
instructions: string | null;
max_output_tokens: number | null;
metadata: Record<string, string>;
parallel_tool_calls: boolean;
reasoning: { effort: string | null; summary: string | null } | null;
text: { format: { type: string; [k: string]: unknown } };
tool_choice: string | object;
tools: object[];
top_p: number;
temperature: number;
truncation: "disabled";
user: string | null;
service_tier: null;
// Always this shape — Sansa Responses never persists a Response object.
store: false;
previous_response_id: null;
// Sansa routing metadata — same shape as chat's `sansa` object,
// plus `cost` (settled USD, since `usage.cost` isn't part of the
// Responses spec). See the Models docs.
sansa?: {
routed: boolean;
routed_model: string | null;
routing_latency_ms: number | null;
cost: number;
};
}Output items
output holds one entry per item the model produced, in generation order (reasoning, then tool calls, then the assistant message — matching whatever order the underlying model actually emitted).
type | Emitted when |
|---|---|
reasoning | reasoning was requested, or the model returned reasoning |
function_call | The model wants to call a function tool |
web_search_call | A web_search tool call returned citations |
shell_call / apply_patch_call / custom_tool_call | A client-executed tool call |
message | The model produced visible text (omitted on tool-only turns) |
{
"id": "rs_...",
"type": "reasoning",
"status": "completed",
"summary": [{ "type": "summary_text", "text": "..." }],
"content": [],
"encrypted_content": "..."
}{
"id": "fc_call_abc123",
"type": "function_call",
"status": "completed",
"call_id": "call_abc123",
"name": "get_weather",
"arguments": "{\"location\":\"Tokyo\"}"
}{
"id": "ws_...",
"type": "web_search_call",
"status": "completed",
"action": {
"type": "search",
"query": "",
"sources": [{ "type": "url", "url": "https://example.com/..." }]
}
}{
"id": "msg_...",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{ "type": "output_text", "text": "...", "annotations": [] }
]
}status on the top-level response is "incomplete" when generation hit max_output_tokens (incomplete_details.reason: "max_output_tokens") or a content filter ("content_filter"); otherwise "completed".
Streaming
Set stream: true. Unlike Chat Completions' data:-only chunks, Responses streaming sends named events:
event: response.created
data: {"type":"response.created","sequence_number":0,"response":{...}}
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"}- Every event's
datahas atypematching theevent:line, and a monotonically increasingsequence_numberstarting at 0. - There is no
data: [DONE]. The stream ends after the terminal event. usageonly appears on the terminal event (response.completedorresponse.incomplete).
See the code panel for a full streaming example with SSE parsing.
Event reference
| Event | When |
|---|---|
response.created | First event. status: "in_progress", empty output, null usage |
response.in_progress | Immediately after created |
response.output_item.added / .done | An output item (message, reasoning, or a tool call) starts / finishes |
response.content_part.added / .done | A content part within a message item starts / finishes |
response.output_text.delta / .done | Incremental / final assistant text |
response.function_call_arguments.delta / .done | Incremental / final JSON for a function call's arguments |
response.reasoning_summary_part.added / .done | A reasoning summary part starts / finishes |
response.reasoning_summary_text.delta / .done | Incremental / final reasoning summary text |
response.completed | Terminal. Full response object including output and usage |
response.incomplete | Terminal, in place of completed, when generation was cut short |
response.failed | Terminal, only if an error occurs after streaming has already started. response.error is set |
Errors before any event is sent are a normal JSON error body (same as non-streaming). Errors after response.created arrive as response.failed — check for that event type instead of relying on HTTP status once the stream has opened.
Tool calling
Round trip is item-based rather than message-based:
- Send a request with
toolsandinput. - The model returns one or more
function_callitems inoutput, each with acall_id. - Run the function yourself.
- Send a new request whose
inputis: the previousinput, plus thefunction_callitem(s) from step 2, plus afunction_call_outputitem per call:
{
"call_id": "call_abc123",
"type": "function_call_output",
"output": "{\"temperature\": 22, \"conditions\": \"sunny\"}"
}call_id is the join key — pass back the exact value from the function_call item. Include tools on every request, not just the first one. output can be a string or an array of input_text parts (concatenated); image parts in tool output are not supported.
Client-executed tools
shell, apply_patch, and custom tools follow the same round trip as function tools, but with typed item names on both sides: shell_call / apply_patch_call / custom_tool_call from the model, and shell_call_output / apply_patch_call_output / custom_tool_call_output for your results. Sansa passes these through so agent runtimes like Codex can execute them locally — Sansa does not execute them itself.
Web search
Pass { "type": "web_search" } (or "web_search_preview") in tools to let the model search the web. Sansa executes this as a hosted tool — you don't implement anything.
- Billed $0.01 per search that returned citations, in addition to the token cost of citation snippets injected into context. A search that returns nothing is not charged.
- Results appear as a
web_search_calloutput item plusurl_citationannotations on the assistant message'soutput_text.annotations. - On
sansa-autowith no other tools, search-only requests route to a search-capable model automatically.
Reasoning
Configure with the reasoning body parameter (see above); effort levels are the same scale as chat — see Reasoning.
When the model returns reasoning, Sansa emits a reasoning output item with a plaintext summary. To carry reasoning state across turns for models that need it, pass include: ["reasoning.encrypted_content"] — the reasoning item then also carries encrypted_content, an opaque blob. Echo it back unmodified on a reasoning input item on your next request:
{
"type": "reasoning",
"summary": [{ "type": "summary_text", "text": "..." }],
"encrypted_content": "..."
}Don't parse, edit, or store this string beyond the round trip — treat it as opaque. It may be omitted even when requested, depending on the model that served the turn; requests work fine without it, reasoning continuity is a quality improvement, not a requirement.
Stateless model
Sansa's Responses endpoint does not persist responses. Every request must carry the full conversation in input.
Accepted (no-ops): omitted store, store: false, omitted previous_response_id, previous_response_id: null. Successful responses always echo store: false and previous_response_id: null.
Rejected with 400 unsupported_parameter:
| Field | Reason |
|---|---|
store: true | No stored responses |
previous_response_id (non-null) | No stored responses to reference |
conversation | Conversations is a separate, unsupported OpenAI product |
background: true | No async job runner |
prompt (stored prompt template ID) | No prompt store |
moderation | No moderation product |
context_management | Compaction requires stored conversations |
An input item with type: "item_reference" is also rejected — it needs a stored item to resolve — but as 400 invalid_request with param: "input", since it's a malformed input array rather than an unsupported top-level field.
The stored-response endpoints below always return 400 rather than 404, so SDKs get a clear message instead of retrying or mis-diagnosing:
GET /v1/responses/{response_id}DELETE /v1/responses/{response_id}GET /v1/responses/{response_id}/input_itemsPOST /v1/responses/{response_id}/cancelPOST /v1/responses/compact
Errors
Errors use the same envelope as the rest of the API. Responses-specific cases:
| HTTP Status | Code | param | When |
|---|---|---|---|
400 | unsupported_parameter | store, previous_response_id, conversation, background, prompt, moderation, context_management | See Stateless model |
400 | invalid_request | input | input is missing, empty, or contains an unsupported item type |
400 | invalid_request | reasoning | reasoning.effort isn't a recognized value |
400 | invalid_request | text | text.format.type is "json_schema" without a name or schema |
400 | unsupported_parameter | input | An input_audio content part was sent (text-only) |
All other error codes — auth, credits, rate limits, invalid model, upstream failures — are identical to Completions; see the full reference in Errors.
Migrating from Chat Completions
Providers, routing, credits, and tool execution are unchanged — Responses is a different request/response shape over the same engine. Field renames:
| Responses | Chat Completions |
|---|---|
input | messages |
instructions | leading system message |
max_output_tokens | max_completion_tokens |
text.format | response_format |
created_at | created |
output[] | choices[0].message |
status | inferred from finish_reason |
usage.input_tokens | usage.prompt_tokens |
usage.output_tokens | usage.completion_tokens |
function tool { name, parameters } | { function: { name, parameters } } |
function_call item call_id | tool_calls[].id |
function_call_output item | { role: "tool" } message |
content input_text / output_text | content text |
content input_image | content image_url |
Keys restricted with an API key endpoint allowlist to chat.completions can already call /v1/responses — the allowlist treats chat.completions as implying responses. To restrict a key to Responses only, use the responses slug directly.
Billing
Same model as Completions: cost is estimated and reserved before the request, then recalculated from actual usage and settled (refunded or deducted) after. Failed requests are not charged. Web search adds the $0.01-per-search charge described above on top of token cost. Settled cost is on sansa.cost — Responses does not use usage.cost since that field isn't part of the OpenAI Responses spec.