Skip to main content

Chat completions

POST /v1/chat/completions is the main inference endpoint. It follows the OpenAI chat completions shape: send a model and a list of messages, get back a completion. The OpenAI SDKs work against it with only a base_url change.

curl https://api.mindshub.ai/v1/chat/completions \
-H "Authorization: Bearer $MINDSHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "sonnet",
"messages": [
{"role": "system", "content": "You are a terse assistant."},
{"role": "user", "content": "What is the capital of Australia?"}
]
}'

Request parameters

ParameterTypeRequiredNotes
modelstringyesA model alias from Models.
messagesarrayyesConversation so far. Roles: system, user, assistant, tool.
streambooleannoDefault false. See Streaming.
max_tokensintegernoOutput token cap. A value above the model's own ceiling is clamped down and reported in X-MindsHub-Clamped-Params; above the hard maximum 131,072 the request is rejected with 400 max_tokens_exceeded. If omitted, a default of 16,384 applies on the Claude family, mindshub_air, deepseek, qwen, glm, and muse-spark; other models use their provider's default. Two sizing notes: the value is reserved against your per-minute token budget up front, and models that reason internally need headroom (see Reasoning effort).
max_completion_tokensintegernoOpenAI's newer spelling. If both are set to positive values, max_completion_tokens wins; a value of 0 is treated as unset.
temperature, top_p, stopvariesnoHandled per model: forwarded where the target model takes the parameter, dropped where it doesn't (named in X-MindsHub-Dropped-Params; stop is reported there as stop_sequences). A value the model restricts still comes back as that provider's 400: kimi takes temperature only at 1 and top_p only at 0.95, and gpt rejects top_p outright. opus, sonnet, fable, and the Gemini models drop temperature and top_p (stop still works on Claude); haiku forwards all three, and Anthropic rejects temperature and top_p together. When in doubt, omit sampling parameters.
stream_optionsobjectno{"include_usage": true} ends the stream with a usage-bearing chunk whose choices array is empty. See Streaming.
toolsarraynoSee Tool calling.
tool_choicestring or objectno"auto", "required", "none", or {"type": "function", "function": {"name": "..."}}. "none" is unreliable on some models, and a forced choice is rewritten rather than rejected on models that restrict it; see Tool calling.
reasoning_effortstringnoSee Reasoning effort. Models that don't take it, or don't take your level, get it dropped or clamped rather than failing the request.
metadataobjectnoAccepted for OpenAI compatibility and ignored. Must be an object if present.

Any other top-level parameter is accepted and silently ignored. Unknown parameters are tolerated for SDK compatibility (coding agents and SDKs treat request rejections as hard errors), and the API does not warn when it ignores one. (This applies to top-level request fields; unknown nested content, like an unrecognized message part type, can reach the provider and fail there.) The ignored parameters that most often matter:

You sendWhat actually happens
response_formatNo guaranteed JSON. Prompt for JSON and validate the output yourself.
nAlways exactly one choice.
seed, presence_penalty, frequency_penalty, logit_bias, logprobs, user, parallel_tool_callsNothing.

Message content

content can be a plain string or an array of parts:

{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR..."}}
]
}

Supported part types are text and image_url. For images:

  • url can be a data: URL (data:<media type>;base64,<data>) or a public http(s) URL, which the upstream provider fetches.
  • JPEG, PNG, GIF, and WebP are passed through. Other inline raster formats (data: URLs) are transcoded to PNG server-side; remote URLs are handed to the provider unchanged.
  • A malformed data URL or invalid base64 returns 400.

Image parts are accepted on every chat model. The catalog doesn't yet flag which models have vision; if the underlying model can't process images, the upstream provider's error is relayed back to you.

The response

{
"id": "chatcmpl-636a4f9b-80c4-4989-a48d-0c9bed215ba7",
"object": "chat.completion",
"created": 1785401283,
"model": "claude-sonnet-5",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Canberra." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 12,
"completion_tokens": 4,
"total_tokens": 16
}
}

Notes on the shape:

  • model is the resolved provider ID, not your alias (why, and what to key on).
  • There is always exactly one choice.
  • message.content is null (not an empty string) when the model produced no text, for example when it only called tools.
  • When the request touched a provider's prompt cache, reads or writes, usage gains prompt_tokens_details: {"cached_tokens": ..., "cache_write_tokens": ...}. Don't treat the field's presence as "a cache read happened": writes alone populate it too. prompt_tokens always includes cached tokens. (cache_write_tokens is a MindsHub extension to the OpenAI shape.) Caching is automatic on most of the catalog; Claude-family targets currently cache only when a request marks cache_control breakpoints on Messages, which Claude Code does for you.

finish_reason

ValueMeaning
stopThe model finished normally.
lengthOutput was truncated at max_tokens (or the model's context limit).
tool_callsThe model stopped to call one or more tools.
content_filterThe upstream provider refused to continue.
null / absentAbnormal end: the provider reported a failure or an unfinished turn. Treat as unsuccessful.

Model-family differences to know:

  • On the Claude family, mindshub_air, deepseek, qwen, glm, and muse-spark, a turn that was truncated and contains tool calls reports length (truncation wins). On Gemini models it reports tool_calls.
  • Gemini models never report null or content_filter: refusals and abnormal ends collapse into stop. Don't build safety-refusal detection on finish_reason for gemini/gemini-flash.
  • kimi responses come through with the provider's own finish reasons, unmapped.

Streaming

Set "stream": true. The response is text/event-stream: a series of data: <json> lines, each carrying a chat.completion.chunk, terminated by data: [DONE].

data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"","role":"assistant"},"index":0}],"created":1785401212,"model":"claude-haiku-4-5-20251001","object":"chat.completion.chunk"}

data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"Hello"},"index":0}],"created":1785401212,"model":"claude-haiku-4-5-20251001","object":"chat.completion.chunk"}

data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop","index":0}],"created":1785401212,"model":"claude-haiku-4-5-20251001","object":"chat.completion.chunk"}

data: [DONE]

Five differences from OpenAI's stream that your client has to handle:

  • Keys with null values are omitted rather than sent as null. Non-terminal chunks have no finish_reason key at all.
  • Usage arrives on request. Send stream_options: {"include_usage": true} and the stream ends with a usage-bearing chunk whose choices array is empty, so guard on chunk.choices being non-empty. kimi streams end with that chunk even without asking, and may carry extra provider fields (system_fingerprint, usage nested in the finish chunk); don't use a strict parser that rejects unknown fields.
  • Tool-call streaming varies by model family. OpenAI- and Claude-family models stream a tool call as an opening delta (with id, type, and the function name) followed by argument fragments. Gemini models deliver each tool call as a single chunk carrying the complete arguments.
  • Errors before the first token are plain JSON. If the request fails before generation starts, you get an ordinary JSON error response with an error status, not an SSE stream, even though you asked for stream: true.
  • A mid-stream error closes the stream without an SSE error event. A stream that ends without a finish_reason chunk was a failed generation, except on Gemini models, which always send one; see Errors → Streaming failures for the full detection rules.

Tool calling

Declare tools the OpenAI way:

{
"model": "sonnet",
"messages": [{"role": "user", "content": "What's the weather in Paris?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}
]
}

When the model calls a tool, the response has finish_reason: "tool_calls" and the assistant message carries the calls:

{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "toolu_01V9g8n42TGjbfJmvecVydN3",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}
}
]
}

Run the tool, then call the API again with the assistant turn you received, followed by one tool message per call, and the same tools array as the first request:

{
"model": "sonnet",
"messages": [
{"role": "user", "content": "What's the weather in Paris?"},
{"role": "assistant", "content": null, "tool_calls": [
{"id": "toolu_01V9g8n42TGjbfJmvecVydN3", "type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}
]},
{"role": "tool", "tool_call_id": "toolu_01V9g8n42TGjbfJmvecVydN3", "content": "18°C, clear"}
],
"tools": [
{"type": "function", "function": {"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}
]
}

Notes:

  • A single response can contain multiple tool calls. Execute them all and return one tool message per call.

  • Echo back tool_calls[].id values exactly as you received them. On Gemini models the IDs carry provider state, and a mismatched ID fails the request.

  • Use role: "tool" for results. (role: "function" is accepted for backward compatibility but not handled as a tool result; don't use it.)

  • tool_choice: "none" is ignored on the Claude family, mindshub_air, deepseek, qwen, and glm: the model may still call tools. It works on the GPT family, gemini/gemini-flash, grok, kimi, and muse-spark. If you need tools off for a turn everywhere, omit tools from that request instead.

  • A forced tool_choice a model can't accept is rewritten rather than rejected. Two models restrict it today, and on both the request is served instead of returning the provider's 400:

    ModelWhat it restrictsWhat we send instead
    kimiA named choice ({"type": "function", …}) 400s while the model's thinking is on, which is the provider default. "required" works.Named → "auto"
    muse-sparkOnly "auto" is accepted; named, "required", and "none" all 400.Named and "required""auto"; "none" honored by dropping tools

    The practical consequence: on these two models a forced choice becomes a strong hint, so the model may answer in prose instead of calling your tool. Keep the "call the tool first" instruction in your prompt, and validate that you got a tool call before relying on one. Unlike dropped and clamped parameters, this rewrite is not reported in a response header, so there is no wire signal — treat the table above as the contract. Non-forcing dict forms that restrict rather than force the callable set are never rewritten.

Two special tool types ask the platform to give the model web access, on models where it's available:

{
"model": "sonnet",
"messages": [{"role": "user", "content": "What changed in the EU AI Act this month?"}],
"tools": [{"type": "web_search"}, {"type": "fetch"}]
}
  • web_search lets the model run web searches; fetch lets it retrieve a URL.
  • On models where search isn't available, the tool entries are dropped silently and the model answers from its own knowledge.
  • Searches carry a per-search charge on top of tokens; see Billing.
  • When your tools array contains only web tools, any tool_choice you send is ignored (forced tool choice can't be applied to server-side search).

Reasoning effort

Models whose catalog entry lists reasoning_efforts accept a reasoning_effort string:

{
"model": "deepseek",
"messages": [{"role": "user", "content": "Prove that √2 is irrational."}],
"reasoning_effort": "high"
}
  • The valid levels per model are in GET /v1/models; they vary (for example sonnet supports low through max, deepseek adds none).
  • Requests never fail over reasoning_effort. A recognized level above a model's ladder is clamped down into it and reported in X-MindsHub-Clamped-Params. A level the ladder can't place (none on models without an off switch, or a typo) is dropped, reported in X-MindsHub-Dropped-Params, and the model's default applies, so send a listed level if cost matters. On models with reasoning_efforts: null the drop currently comes with no header.
  • If you send nothing, the model's default_reasoning_effort applies, which for most reasoning models is not "off". Send the lowest listed level explicitly if you want speed over depth.
  • Reasoning content is never returned; you get the final answer. Reasoning tokens are billed as output tokens.
  • Some models reason internally even though the level isn't adjustable (mindshub_air, kimi). Budget max_tokens generously for them (a few hundred tokens of headroom) or the reasoning uses the cap and the visible answer arrives truncated with finish_reason: "length". More in Models → Reasoning effort.

What surprises people

  • Funding is checked before the model runs. An empty wallet or exhausted allowance refuses the request up front (402 or 429) at no cost; admission checks your balance, not the request's estimated size. See Billing.
  • There is no explicit failover indicator. During a provider outage the platform may retry or serve your request through a fallback route. The resolved model in the response may reveal a changed route, but don't treat it as a reliable failover signal.

The same models and features are available on POST /v1/responses, the shape OpenAI Codex speaks, and on Messages for the Anthropic ecosystem. Choosing an API compares the three.