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.

Base URL: https://api.mindshub.ai/v1. The same first call is below in curl, Python, and JavaScript. The two SDK tabs use the official OpenAI client: pip install openai or npm install openai.

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

Three notes on running these as they are:

  • Set MINDSHUB_API_KEY to a key from console.mindshub.ai. The API key is the only value you have to supply.
  • The samples call mindshub_air, the alias your monthly included tokens cover, so they work on an account with no payment method on file. Change the model string to any alias in Models once you have a wallet balance; nothing else about the request changes.
  • The JavaScript sample uses top-level await, so run it as an ES module: a .mjs file, or "type": "module" in your package.json.

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.
response_formatobjectnoStructured output: {"type": "json_schema", "json_schema": {...}}, {"type": "json_object"}, or {"type": "text"}. See Structured output. The one parameter that is rejected rather than dropped when a model can't honor it — prose handed to a caller who will json.loads() it is a different kind of answer, not a differently-flavored one. json_object is a 400 on the Claude family, which has no schema-less JSON mode.
parallel_tool_callsbooleannoHonored, including on the Claude family, which spells it inverted. Gemini models can't express it and report it as dropped.
top_kintegernoA MindsHub extension, not an OpenAI parameter. Forwarded where the model takes it; always dropped on the GPT and Grok families.
seedintegernoBest-effort determinism, honored on gemini/gemini-flash and kimi and dropped elsewhere. Never a reproducibility guarantee even where it applies.
presence_penalty, frequency_penaltynumbernoAs seed.
verbositystringno"low", "medium", or "high" — how much prose to spend on an answer of the same substance, as distinct from reasoning_effort, which buys thinking. Honored on the GPT family; dropped elsewhere, including on grok.
web_search_optionsobjectnoEnables web search, the same as sending {"type": "web_search"} in tools. Its search_context_size and user_location sub-options are dropped. See Built-in web search.
functions, function_callvariesnoOpenAI's pre-tools spelling, deprecated by OpenAI but still served by them and by us. See Legacy function calling. Send tools alongside either and the request is treated as modern.
metadataobjectnoAccepted for OpenAI compatibility and ignored. Must be an object if present.

Every parameter OpenAI defines is accepted, and any other top-level parameter is accepted and ignored — unknown parameters are tolerated for SDK compatibility, since coding agents and SDKs treat request rejections as hard errors. (This applies to top-level request fields; unknown nested content, like an unrecognized message part type, can reach the provider and fail there.)

What the API can't honor, it names in X-MindsHub-Dropped-Params rather than discarding silently:

You sendWhat actually happens
nAlways exactly one choice. Reported as dropped unless you sent 1.
logprobs, top_logprobs, logit_bias, prediction, modalities, audioNothing, and reported as dropped. No model behind this endpoint can return log probabilities or accept a bias map.
user, safety_identifier, service_tier, store, prompt_cache_key, prompt_cache_retentionNothing, and not reported — these describe you or your account rather than the generation, so nothing about the answer changes. Requests are attributed by API key; caching is automatic.

The full per-parameter contract lives in the codebase, in PARAM_SUPPORT in minds/requests/chat_completions_request.py, and is held to the OpenAI SDK's own parameter list by a test.

Message content

Roles are system, user, assistant, and tool. developer is accepted and read as system — it is OpenAI's newer spelling, recommended for reasoning models, and the two mean the same thing here.

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, so it changes with the model you asked for, and it changes again whenever we repoint an alias. The response above came from a sonnet request. What each alias resolves to today, and what to key on instead, are both on the models page, which is the one place that mapping is stated.
  • 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.
  • message.refusal carries the model's stated reason when it declined, on the models whose provider gives one (the Claude and GPT families). It is absent or null — never an empty string — when the model refused without saying why, because those are different facts. finish_reason is content_filter either way.
  • usage.completion_tokens_details is on every response, as it is on OpenAI's. Its reasoning_tokens is a subset of completion_tokens, not an addition to it, and is broken out by the GPT/Grok families and Gemini models. Elsewhere it reads 0, which means no separately-reported reasoning rather than no reasoning: the Claude family bills thinking as output without separating it, so a Claude turn reports 0 however much it thought. Don't read 0 as "this model doesn't reason" — Models is the place that answers that.
  • service_tier is always "default". There is one serving tier; the field exists so a client reading it gets a string rather than a missing key.
  • 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 MindsHub marks the provider's cache breakpoints on the stable prefix for you, and on the rest the provider caches implicitly. A request that sends its own cache_control breakpoints, as Claude Code does on Messages, is passed through untouched.

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.

A 400 from a malformed request body comes back in the standard OpenAI error shape — {"error": {"message", "type", "param", "code"}}, with param naming the field — so client.chat.completions.create(...) raises BadRequestError. (Until 2026-09 it was a 422 carrying FastAPI's {"detail": [...]}, which raised UnprocessableEntityError and had no param to branch on.)

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 honored everywhere. On the Claude family it is sent as Anthropic's own none; on mindshub_air, deepseek, qwen, glm, and muse-spark, whose upstreams have no word for it, it is honored by dropping the tools from that request instead — reported as tool_choice=none>dropped in X-MindsHub-Clamped-Params. (Until 2026-09 it was silently ignored on the Claude family and the model could still call tools.)

  • 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. The rewrite is reported on X-MindsHub-Clamped-Params, in the same name=requested>applied form as any other clamp: tool_choice=required>auto, tool_choice=named>auto, or tool_choice=none>dropped when the tools were dropped instead. The values are category labels rather than your literal tool_choice, so a named choice reports as named. Non-forcing dict forms that restrict rather than force the callable set are never rewritten.

Legacy function calling

functions and function_call are OpenAI's original function-calling fields, deprecated in favour of tools/tool_choice — their own SDK marks them so. They still work on OpenAI, and they work here, in all four places OpenAI switches shape when you use them:

You send / receive
Requestfunctions: [{name, description, parameters}], function_call: "auto" | "none" | {"name": ...}
Responsemessage.function_call: {name, arguments}, finish_reason: "function_call"
Streamingdelta.function_call — the name on the opening chunk, then arguments fragments
Replaythe assistant turn with its function_call, then {"role": "function", "name": ..., "content": ...}

Two limits worth knowing:

  • One call per turn. The legacy field is a single object, so a model that wants two calls has the extras dropped. That limitation is the shape's own, and the reason OpenAI replaced it — use tools if you need parallel calls.
  • Sending tools alongside functions is read as a modern request and answered with tool_calls. If you want the legacy shape back, send only the legacy fields.

New code should use Tool calling above. These fields exist so existing code doesn't have to change.

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).
  • OpenAI's web_search_options parameter does the same thing as a {"type": "web_search"} tool entry, so either spelling works. Its search_context_size and user_location sub-options have no cross-provider equivalent and are dropped.

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 takes low, high and max, and gpt-mini adds none). Read the model's own list rather than assuming a family shares one ladder: deepseek listed none until 2026-08-18, when the alias moved to DeepSeek-V4-Pro-0813, whose three levels do not include it.
  • Requests never fail over reasoning_effort. A recognized level outside a model's ladder is clamped onto it: below the floor, including none on a model without an off switch, you get the cheapest supported rung (which can cost more than the unsupported level you requested); above the ceiling you get the top rung; between rungs you get the lower one. Clamps are reported in X-MindsHub-Clamped-Params. Only an unrecognized value such as a typo is dropped and reported in X-MindsHub-Dropped-Params, letting the model's default apply. 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.