Responses API
POST /v1/responses speaks the OpenAI Responses API, the shape the current
OpenAI SDKs reach for by default. If your code calls
client.responses.create(...), pointing it at Sable is the same one-line swap
as everything else:
from openai import OpenAI
client = OpenAI(
base_url="https://api.buildsable.com/v1",
api_key="sk-sable_...",
)
resp = client.responses.create(
model="sable",
instructions="Answer in one sentence.",
input="What is a signed receipt?",
)
print(resp.output_text)
It is the chat path underneath
The handler translates your request into a chat-completions request, runs the existing chat handler in-process, and translates the result back. It never opens an upstream connection of its own. That is deliberate: everything that makes the chat path correct — sealing on ingress, credit authorization and holds, per-key scopes and spend caps, policy-as-code, metering, signed receipts, provider failover, and the confidential fail-closed guard — therefore applies here for free and cannot drift out of sync with a second implementation.
Practically: a /v1/responses call is billed, capped, and receipted exactly
like the equivalent /v1/chat/completions call. The x-sable-receipt headers
come back unchanged.
Request fields
| Field | Notes |
|---|---|
model | A Sable model id. Same catalog as chat. |
input | A string (one user turn), or an array of input items. |
instructions | Becomes the leading system message. |
max_output_tokens | The Responses spelling of max_tokens. |
temperature, top_p | Passed through. |
tools | function tools only — see below. |
tool_choice | "auto" / "required" / "none" / {type:"function", name}. |
stream | true emits Responses SSE events. |
metadata | Echoed back on the response object. |
Every Sable request field works here too:
sable_privacy_tier, sable_region, sable_scrub, sable_run_id, and
sable_context.
Input items may be message objects ({role, content}), bare strings, a
function_call (an assistant turn that requested a tool), or a
function_call_output (the tool's result). Text parts are flattened; image and
file parts reach the model in their original form.
What is not supported, and why
previous_response_id returns a 400. It is not silently ignored. Server-
side conversation state would mean persisting your prompts and completions, and
Sable never does that — so there is genuinely nothing on our
side to resume from. Send the prior turns in input, the way the chat surface
works. Refusing is the honest answer; pretending to continue a conversation we
did not keep would not be.
Hosted tools return a 400. web_search, file_search, and
code_interpreter run on the vendor's own infrastructure, not on this gateway.
A caller who asked for a web search and got an answer with no search performed
has been misled, so these are refused rather than dropped. function tools —
the ones your own code executes — translate cleanly and are fully supported.
For running code, Sable has metered sandboxes, which are
receipted like everything else.
Streaming
With stream: true the response is a Responses event stream:
event: response.created
event: response.in_progress
event: response.output_item.added
event: response.content_part.added
event: response.output_text.delta ← repeated
event: response.output_text.done
event: response.content_part.done
event: response.output_item.done
event: response.completed
Tool calls stream as response.output_item.added followed by
response.function_call_arguments.delta / .done. A response cut short by
max_output_tokens ends with response.incomplete and an
incomplete_details.reason instead of response.completed.
Sable's own trailing sable.receipt event passes through untouched. An
OpenAI-shaped client ignores the unknown event type; a Sable-aware one still
gets its receipt, since response headers are already on the wire by the time
the final token count is known.
In batches
/v1/responses is one of the three endpoints a batch may
target, alongside /v1/chat/completions and /v1/embeddings.