Responses
POST /v1/responses implements the OpenAI Responses API. The OpenAI SDKs work against it with a base_url change, and, as everywhere else on MindsHub, any model in the catalog can serve a Responses request, not just OpenAI's. This is also the format OpenAI Codex speaks.
Requests and responses use the Responses shapes throughout: SDK helpers like response.output_text and client.responses.stream() work, function-tool round trips (function_call out, function_call_output back in) work on any model, and turns can be chained by previous_response_id instead of resending the conversation.
Two gaps worth knowing before you start: structured output (text.format) is not implemented anywhere on MindsHub yet — it is reported in X-MindsHub-Dropped-Params rather than honored, so a request asking for a JSON schema gets prose. And OpenAI's separate Conversations API (the conversation parameter) is not supported; it is rejected with a 400.
curl https://api.mindshub.ai/v1/responses \
-H "Authorization: Bearer $MINDSHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "sonnet",
"input": "What is the capital of Australia?"
}'
- Python
- TypeScript
from openai import OpenAI
import os
client = OpenAI(
base_url="https://api.mindshub.ai/v1",
api_key=os.environ["MINDSHUB_API_KEY"],
)
response = client.responses.create(
model="sonnet",
input="What is the capital of Australia?",
)
print(response.output_text)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.mindshub.ai/v1",
apiKey: process.env.MINDSHUB_API_KEY,
});
const response = await client.responses.create({
model: "sonnet",
input: "What is the capital of Australia?",
});
console.log(response.output_text);
Request parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
model | string | yes | A model alias from Models. |
input | string or array | yes | A plain string, or an array of input items. See Input. |
instructions | string | no | System-level guidance. Becomes a leading system message. |
stream | boolean | no | Default false. See Streaming. |
tools | array | no | Function tools and web_search. See Tools. |
tool_choice | string or object | no | "auto", "required", "none", or {"type": "function", "name": "..."}. On models that restrict forced choice a named or "required" choice is rewritten rather than rejected; see Chat completions → Tool calling for the per-model table. |
max_output_tokens | integer | no | Output cap. |
reasoning | object | no | effort has the same semantics as reasoning_effort; summary asks for a summary on the returned reasoning item. |
temperature, top_p | number | no | Honored where the target model supports them, dropped where it doesn't. See Parameter adaptation. |
store | boolean | no | Default true, as on OpenAI. See Conversation state. |
previous_response_id | string | no | Chains this turn onto a stored one. See Conversation state. |
include | array | no | "reasoning.encrypted_content" carries a reasoning model's thinking across turns; other values are ignored. |
Accepted and ignored: text, metadata, background (requests are always served synchronously), and any unknown top-level field. conversation, prompt and context_management are rejected with a 400 naming the field rather than silently ignored — a client that believes we are holding its thread would otherwise stop sending history and lose context on every turn.
Input
input takes a plain string, or an array of items for multi-turn conversations and richer content:
{
"model": "sonnet",
"input": [
{"role": "user", "content": "What's in this image?"},
{"role": "user", "content": [
{"type": "input_text", "text": "Describe the chart."},
{"type": "input_image", "image_url": "https://example.com/chart.png"}
]}
]
}
Supported item content types are input_text, output_text, and input_image. Tool round-trips use function_call and function_call_output items; see Tools.
Roles are system, user, and assistant. Prefer instructions over a system item for system-level guidance; both work.
The response
{
"id": "resp_9f2c1ae0b4d8",
"object": "response",
"created_at": 1785401283,
"status": "completed",
"model": "sonnet",
"output": [
{
"type": "message",
"id": "msg_1c40a2",
"role": "assistant",
"status": "completed",
"content": [
{"type": "output_text", "text": "Canberra.", "annotations": []}
]
}
],
"output_text": "Canberra.",
"usage": {
"input_tokens": 12,
"input_tokens_details": {"cached_tokens": 0},
"output_tokens": 4,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 16
}
}
Notes on the shape:
output_textis the flattened assistant text, the same convenience field the OpenAI SDK exposes.outputcarries the structured items.outputcontainsmessageitems and, when the model calls tools,function_callitems.modelechoes back the alias you sent, unlike Chat Completions, which reports the resolved provider ID.input_tokens_details.cached_tokensreports prompt cache reads. Caching is automatic on most of the catalog; the Claude family currently caches only viacache_controlbreakpoints on Messages.output_tokens_details.reasoning_tokensis always0. Reasoning bills as output but isn't broken out separately here:output_tokensincludes it.
Streaming
Set "stream": true and you get the typed Responses event stream, with event: names matching the payload type:
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
The SDK's streaming helper parses this natively:
stream = client.responses.create(
model="sonnet",
input="Count to five.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
Tool calls stream as response.function_call_arguments.delta events between an output_item.added and output_item.done pair for the function_call item.
Every event carries a monotonic sequence_number, so a client can detect a gap, and output items are strictly sequential: item N is closed before item N+1 opens. Text that resumes after a tool call opens a second message item rather than reopening the first.
The final response.completed event carries the complete response object, including real token usage. Three behaviors to code around:
- A request that fails before generation starts returns a JSON error, not an SSE stream, even with
stream: true. Check the responseContent-Typebefore parsing. - A mid-stream failure is reported in-band as a
response.failedevent, because the HTTP status is already200by then. It carriesresponse.error.code—rate_limit_exceeded,context_length_exceeded,insufficient_quota,invalid_promptorserver_error— plus whatever output was produced before the failure, markedincomplete.response.failedandresponse.completedare mutually exclusive. - A truncated turn terminates with
response.incomplete, notresponse.completed, withincomplete_details.reasonset tomax_output_tokensorcontent_filter.
Tools
Function tools use the Responses flat shape: name and parameters at the top level of the tool object, not nested under function:
{
"model": "sonnet",
"input": "What's the weather in Lisbon?",
"tools": [{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}]
}
The model's call arrives as a function_call item in output:
{"type": "function_call", "id": "fc_a91b", "call_id": "call_a91b",
"name": "get_weather", "arguments": "{\"city\": \"Lisbon\"}"}
Return the result by appending both the function_call item and a function_call_output item to input, then calling again:
{
"model": "sonnet",
"input": [
{"role": "user", "content": "What's the weather in Lisbon?"},
{"type": "function_call", "call_id": "call_a91b",
"name": "get_weather", "arguments": "{\"city\": \"Lisbon\"}"},
{"type": "function_call_output", "call_id": "call_a91b", "output": "19°C, light rain"}
],
"tools": [{"type": "function", "name": "get_weather", "description": "...", "parameters": {}}]
}
Echo call_id back exactly as received, and send the same tools array on the follow-up.
Built-in web search
web_search maps onto the platform's web search on models where it's available:
{
"model": "sonnet",
"input": "What changed in the EU AI Act this month?",
"tools": [{"type": "web_search"}]
}
On models without search, the tool entry is dropped and the model answers from its own knowledge. Searches carry a per-search charge; see Billing. A forced tool_choice can't be applied to server-side search and is ignored when your tools array holds only web tools.
Conversation state
Responses are stored by default, so you can chain turns by id instead of resending the conversation:
first = client.responses.create(model="sonnet", input="My name is Quill.")
second = client.responses.create(
model="sonnet",
input="What's my name?",
previous_response_id=first.id,
)
Send store=False to keep a turn out of storage entirely; its id is then not retrievable and cannot be chained from. Resending the full conversation in input every turn also remains valid, and is what the OpenAI SDK does when you aren't chaining. Prompt caching keeps that cheap on most models: repeated prefixes bill at roughly a tenth of the input rate.
Stored turns are readable and removable:
| Call | Behavior |
|---|---|
client.responses.retrieve(id) | The response as it was returned, output items included. |
client.responses.input_items.list(id) | That turn's own input items — not the inherited history. |
client.responses.delete(id) | Hard delete. The content is gone, not flagged. |
Three boundaries worth knowing:
- Stored turns expire after 30 days. After that the id is a
404and chains through it shorten. - A chain is walked at most 25 turns back. Beyond that, and when an older turn has expired or been deleted, the request is still served with the history that remains and the response carries
X-MindsHub-Chain-Truncated. - An unknown
previous_response_idis a404, not a silently context-free answer.
instructions are per turn and are deliberately not carried across previous_response_id, matching OpenAI — send them again on each request if you want them to persist.
The conversation parameter (OpenAI's separate Conversations API) is not supported and is rejected with a 400.
Parameter adaptation
As on every MindsHub endpoint, parameters the target model doesn't support are dropped rather than rejected, and the response reports what changed:
| Header | Meaning |
|---|---|
X-MindsHub-Dropped-Params | Parameters removed for this model, e.g. top_k |
X-MindsHub-Clamped-Params | Values adjusted to the model's range, as name=requested>applied |
Neither header appears when nothing was changed. Details and boundaries in Core concepts.
Errors
Errors use the OpenAI envelope, so SDK error handling works unmodified:
{
"error": {
"message": "The model 'foo' does not exist or you do not have access to it.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}
Status codes and their meanings are shared across the API; see Errors.
Using Codex
Codex speaks this format exclusively, so wire_api = "responses" runs it on any catalog model, including Claude and Kimi. See Coding agents for the config, or MindsHub in Codex for the full guide.