# MindsHub Inference > Call every major model through the OpenAI, Responses and Anthropic APIs, with one key and one bill. This file contains all documentation content in a single document following the llmstxt.org standard. ## Cancel a background response Only a response created with `background: true` can be cancelled, as on OpenAI. Every request completes synchronously, so a stored background response is already terminal and this returns it unchanged; any other response is a 400. Request --- ## chat OpenAI Chat Completions. `POST /v1/chat/completions`. ```mdx-code-block import DocCardList from '@theme/DocCardList'; import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; ``` --- ## Count tokens Prompt tokens a Messages request would consume. Exact for a concrete Claude model name; an estimate from the platform's Claude counting model for any other alias. Free and unmetered. Request --- ## Create a chat completion OpenAI Chat Completions, on any model in the catalog. Send `model` (an alias) and `messages`; set `stream: true` for `text/event-stream` chunks terminated by `data: [DONE]`. Parameters the target model cannot take are dropped and named in `X-MindsHub-Dropped-Params`, and parts of the request the target cannot carry in `X-MindsHub-Dropped-Content` (a stream whose 200 went out early carries neither); `response_format` is the one thing refused with a 400 instead. The response's `model` names the model that served (`mindshub_air` and `mindshub_blaze` return their own alias), and it can be sent back as-is. Request --- ## Evaluate typed decisions Evaluate named noul, choice, or score questions against shared state. Authenticate with a MindsHub key. TypeSafe hosts Jev; no TypeSafe key is needed by callers. Each question sees the same state and returns an answer under its original name. The response model is the actual served version. Returns one JSON response; streaming, tools, and conversation chaining are not supported. Input and output are priced at $0 during the Jev launch promotion. Organizations with wallet credit, or with a card that has completed a top-up and has no payment error, call Jev without a daily cap. Other organizations get a free daily allowance while shared free capacity lasts; past it the request is refused with 402 wallet_empty. Rate limits apply to every organization. Allowance: https://docs.mindshub.ai/inference/billing#jev-decisions Guide: https://docs.mindshub.ai/inference/decision-models Examples, limits, and retries: https://docs.mindshub.ai/inference/decisions Request --- ## Create embeddings OpenAI embeddings on an embedding alias (`embed-small`). Always bills to the wallet, never to the included allowance. The response's `model` names the model that served. Request --- ## Create a message Anthropic Messages, on any model in the catalog. Authenticate with `Authorization: Bearer` (the SDK's `auth_token`), never `x-api-key`. `model` takes a catalog alias or a real Claude model name. Set `stream: true` for the Anthropic event sequence. The response's `model` names the model that served (`mindshub_air` and `mindshub_blaze` echo what you sent). Request --- ## Create a response OpenAI Responses, on any model in the catalog. Send `model` and `input`; set `stream: true` for the typed `response.*` event stream. Turns are stored for 30 days unless `store: false` and chain by `previous_response_id`. `conversation`, `prompt` and `context_management` are refused with a 400. The response's `model` names the model that served (`mindshub_air` and `mindshub_blaze` return their own alias). Request --- ## decisions Jev typed decisions served by TypeSafe. ```mdx-code-block import DocCardList from '@theme/DocCardList'; import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; ``` --- ## Delete a stored response Hard delete: the content is gone, not flagged. Deleting an absent id is a 404, as on OpenAI. Request --- ## embeddings OpenAI embeddings on the embedding aliases. ```mdx-code-block import DocCardList from '@theme/DocCardList'; import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; ``` --- ## Retrieve a stored response The response as it was returned, output items included. Stored turns expire after 30 days. Request --- ## Get a trace One trace with its span waterfall and payloads. Content is visible only to the member who made the request. 403 when you can see the organization's traces but this one is another member's; 404 when it does not exist or is not visible to you. Request --- ## List models Every alias in the catalog with the metadata a model picker needs, including whether your organization can call it right now. Free to call, never paginated. Both the OpenAI and Anthropic SDKs' `models.list()` parse it. Claude Code and Codex receive client-specific shapes, detected from their headers. --- ## List a stored response's input items That turn's own input items, not the inherited history. Paginated with `after`. Request --- ## List traces Per-request records for your organization, newest first. Members see their own requests; admins see the organization's. Available where the organization has traces enabled; otherwise a 404. Request --- ## messages Anthropic Messages, the lane Claude Code speaks. ```mdx-code-block import DocCardList from '@theme/DocCardList'; import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; ``` --- ## MindsHub Inference API Every major model through the three request formats you already use: OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages, on one key and one bill. Any model in the catalog can serve any of the three when its kind is chat. Jev uses the native state/questions shape on POST /v1/decisions. Authenticate with `Authorization: Bearer mdb_…`. OpenAI SDKs take the base URL `https://api.mindshub.ai/v1`; Anthropic SDKs take `https://api.mindshub.ai` and authenticate with `auth_token`, not `api_key`. On the three chat APIs, parameters the target model cannot take are dropped and named in `X-MindsHub-Dropped-Params`; values outside its range are clamped and named in `X-MindsHub-Clamped-Params`. Parts of the request itself the target cannot carry — a content block, a per-block key, a tool schema keyword — are likewise dropped and named in `X-MindsHub-Dropped-Content`, so a model that does not support something you sent serves the request instead of refusing it. Structured output is the one parameter refused (400) rather than dropped. Guides: https://docs.mindshub.ai/inference/ **"HTTP: Bearer Auth"** `Authorization: Bearer mdb_…` Security Scheme Type: http HTTP Authorization Scheme: bearer --- ## models The catalog your key can see. ```mdx-code-block import DocCardList from '@theme/DocCardList'; import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; ``` --- ## responses OpenAI Responses, with server-side conversation state. ```mdx-code-block import DocCardList from '@theme/DocCardList'; import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; ``` --- ## traces Per-request records for your organization. Enabled per organization. ```mdx-code-block import DocCardList from '@theme/DocCardList'; import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; ``` --- ## Authentication Every request needs a bearer token in the `Authorization` header: ``` Authorization: Bearer mdb_xxxxxxxx.yyyyyyyyyyyyyyyyyyyy ``` That is the only authentication header the API accepts. In particular, the Anthropic-style `x-api-key` header is **not** accepted anywhere, including on the Anthropic-compatible `/v1/messages` endpoint. See [Anthropic compatibility](https://docs.mindshub.ai/inference/messages.md) for what that means for Anthropic SDKs. ### API keys Keys are created in the [console](https://console.mindshub.ai) and have the format `mdb_.`. What to know about them: - **The full key is shown once**, when you create it. Afterwards the console shows only the prefix. If you lose a key, revoke it and create a new one. - **Keys belong to you, inside your organization.** Usage and billing accrue to the organization, but a teammate cannot see or revoke your keys. - **You can have up to 50 active keys** at a time. Creating more returns a `409` with code `api_key_limit_reached`. - **Keys can carry an expiry date**, set at creation. No expiry means the key lives until revoked. - **Deleting a key revokes it.** Revocation is immediate and permanent. - **Billing problems don't revoke keys.** If your organization's wallet runs dry, requests are refused (see [Errors](https://docs.mindshub.ai/inference/errors.md)) but the key stays valid, and access resumes automatically once the balance recovers. Keys are created through the console (which uses your browser session's credentials). The contract that matters for automation: **an API key can never mint another API key.** ### What a failed authentication looks like Authentication is checked before the request reaches the API, so a missing, malformed, or revoked key returns an **HTTP 401 carrying the standard error envelope**, on every endpoint including the OpenAI-compatible ones: ```json {"type": "error", "error": {"type": "authentication_error", "message": "Invalid or missing credentials. See https://docs.mindshub.ai/inference/authentication"}} ``` Treat any 401 as "bad credentials" and do not retry it. The check accepts a browser session as well as an API key, which is why the message says credentials rather than key. A `403` arrives in the same shape with `error.type` of `permission_error`: the credential is real, but it is not allowed to use that resource. A key whose user has left the organization it belongs to is the usual cause. Do not retry that either. See [Errors](https://docs.mindshub.ai/inference/errors.md). Your credentials are checked twice, once at the edge and once at the deeper authorization gate every request passes on its way to a model. Both produce the same body shape and the same statuses, so you do not need to tell them apart. Everything after authentication (unknown model, empty wallet, rate limits) returns structured JSON as described in [Errors](https://docs.mindshub.ai/inference/errors.md). ### Checking your account from code Two account endpoints accept API keys and report your remaining allowance percentage, per-model usage, and (for the billing owner) the wallet balance. They live on a separate host, `auth.mindshub.ai`. The full request and response shapes are in [Billing → Checking usage and balance from code](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code). ### Key hygiene - Keep keys out of source control and client-side code. Anyone with the key can spend your organization's wallet. - Use one key per application or environment, and name keys in the console so a leaked one is easy to identify and revoke. - Rotate by creating the new key first, deploying it, then revoking the old one; revocation is immediate. --- ## Billing MindsHub Inference is pay-as-you-go, with no subscription. Two things fund your requests: **Included allowance.** An organization with a free grant receives a recurring allowance for `mindshub_air`; one without a grant has an allowance of zero. It charges four token lanes at different weights, so efficient prompt caching preserves more useful work than uncached input or generated output. The window has a fixed duration and `next_refresh_at` gives its exact end. Programmatically, use `included_percent_remaining`; the old `included_tokens` object remains temporarily for compatibility and carries no token count despite its name. **The wallet.** A prepaid, organization-level credit balance that funds paid models, embeddings, and web-search charges. Credits never expire. Jev is priced at $0 during its launch promotion; organizations without credit call it within a free daily allowance, described under [Jev decisions](#jev-decisions). A request passes the funding check when your wallet has available balance, when it uses `mindshub_air` with allowance remaining, or when it calls Jev and your organization has a card with a completed top-up and no payment error, or still has free Jev allowance left ([Jev decisions](#jev-decisions)). Authentication, access restrictions and rate limits still apply. Otherwise it is refused up front with `402 wallet_empty` or `429 included_allowance_exhausted`. Allowance exhaustion returns the same ISO-8601 `reset_at` in the JSON body and `X-MindsHub-Reset-At` header. An organization whose included allowance is zero (never granted, or set to zero) gets the same `429` with neither, because nothing refills; adding credit is the only way forward. Both billing `429`s carry `x-should-retry: false`, so the OpenAI and Anthropic SDKs do not retry them. A refused request consumes nothing. ### How the wallet works - **Top up** from the [console](https://console.mindshub.ai). Minimum top-up is $10. - **Auto-recharge** (optional): set a threshold and a target: when the balance crosses the threshold, your card is charged back up to the target, typically within a few minutes. A monthly recharge cap (default $100) limits automatic charges; hitting it pauses recharging until the calendar month rolls over. - **Your card is only charged for top-ups and recharges.** Usage consumes prepaid credits. - A declined auto-recharge stops recharging until the billing owner fixes the payment method; it never retries a declined card on its own. Requests keep working until the balance runs out. - Wallet visibility is role-based: the billing owner sees balances and dollar figures; other members see their own usage in tokens only. - An empty wallet never revokes API keys ([Authentication](https://docs.mindshub.ai/inference/authentication.md)); requests resume automatically once credit lands. ### What gets metered Four token counters, per request: | Counter | What it is | Billed at | |---|---|---| | Input tokens | Prompt tokens not served from cache | The model's input rate | | Output tokens | Everything the model generates, including internal reasoning | The model's output rate | | Cached input tokens | Prompt tokens read from the provider's prompt cache | The model's cached-input rate, roughly a tenth of input on most of the catalog but not uniformly so. Read it off the [price list](#price-list) rather than deriving it | | Cache-write tokens | Prompt tokens written into the cache | The model's cache-write rate: the input rate on most models, 1.25× input on the Claude family, `mindshub_air`, and the GPT-6 and GPT 5.6 lines. Not billed at all on the Gemini models or on `mindshub_blaze`, which shows `n/a` in the price list | On chat models, prompt caching is automatic and you never have to ask for it. On most of the catalog MindsHub marks the provider's cache breakpoints on the stable prefix of your request; on the rest the provider caches implicitly and there is nothing for us to mark. A request that already carries its own `cache_control` breakpoints, as Claude Code sends on [Messages](https://docs.mindshub.ai/inference/messages.md), is passed through exactly as you sent it. You see the effect as `cached_tokens` in responses and a lower effective input price. One rough edge: on Claude targets a system prompt sent as an array of content parts rather than a plain string gets no breakpoint of its own. The rolling breakpoint on the previous message picks it up from the second turn onward, so the cost is one extra full-price turn rather than every turn. How this interacts with the included allowance: - On `mindshub_air`, **cached reads draw 1 weighted unit per token**, uncached input draws 10, cache writes draw 12, and output draws 60. - **A web search on `mindshub_air` draws 65,000 weighted units** and bills to the wallet only once the allowance is spent. On its own that is room for about 100 searches per window; the search results the model reads also draw the allowance as input tokens, so a real session fits fewer. - The weights are allowance policy, not prices. Wallet billing still uses the raw token counters and the current price list. - **Page fetches, embeddings, and web searches on every other model always bill to the wallet**, even when the request's model tokens were covered by the allowance. The full matrix is the [summary table](#summary-of-what-bills-where) at the bottom. ### Price list These are MindsHub's prices; they track the upstream providers' list prices except for Jev, which is priced at $0 during its launch promotion. Per **million tokens**, with rates from October 2026 (the console is the authoritative, current source): A **fixed alias naming the same model as its moving alias** — `gpt-6-astra` against `gpt`, `sonnet-5` against `sonnet` — is priced identically to it and is left out of this table. Pinning a model costs nothing; it only stops the alias moving. Aliases for **older** releases price separately and are listed. | Model | Input | Output | Cached input | Cache write | |---|---|---|---|---| | `mindshub_air` | $0.10 | $0.50 | $0.01 | $0.125 | | `mindshub_blaze` | $0.99 | $1.49 | $0.99 | n/a | | `gpt-oss-fireworks` | $0.15 | $0.60 | $0.02 | $0.15 | | `sonnet` | $2.00 | $10.00 | $0.20 | $2.50 | | `sonnet-5` | $2.00 | $10.00 | $0.20 | $2.50 | | `opus` | $4.00 | $20.00 | $0.20 | $5.00 | | `opus-5` | $5.00 | $25.00 | $0.50 | $6.25 | | `fable` | $10.00 | $50.00 | $0.25 | $12.50 | | `fable-5` | $10.00 | $50.00 | $1.00 | $12.50 | | `haiku` | $1.00 | $5.00 | $0.10 | $1.25 | | `gpt` | $10.00 | $50.00 | $1.00 | $12.50 | | `gpt-5-6-sol` | $5.00 | $30.00 | $0.50 | $6.25 | | `gpt-sol` | $2.00 | $10.00 | $0.10 | $2.50 | | `gpt-sol-6` | $2.00 | $10.00 | $0.20 | $2.50 | | `gpt-terra` | $2.00 | $12.00 | $0.20 | $2.50 | | `gpt-luna` | $0.10 | $0.50 | $0.01 | $0.125 | | `gpt-luna-5-6` | $0.20 | $1.20 | $0.02 | $0.25 | | `gpt-codex` | $1.75 | $14.00 | $0.18 | $1.75 | | `gpt-mini` | $0.75 | $4.50 | $0.08 | $0.75 | | `gpt-nano` | $0.20 | $1.25 | $0.02 | $0.20 | | `gemini` | $2.00 | $12.00 | $0.20 | n/a | | `gemini-flash` | $0.75 | $3.75 | $0.08 | n/a | | `gemini-flash-3-7` | $0.75 | $3.75 | $0.08 | n/a | | `gemini-flash-3-6` | $0.75 | $3.75 | $0.08 | n/a | | `gemini-flash-3-5` | $1.50 | $9.00 | $0.15 | n/a | | `gemini-flash-3` | $0.50 | $3.00 | $0.05 | n/a | | `gemini-flash-lite` | $0.25 | $1.50 | $0.03 | n/a | | `kimi` | $3.00 | $15.00 | $0.30 | $3.00 | | `deepseek-v4-flash` | $0.30 | $1.20 | $0.006 | $0.30 | | `qwen` | $2.00 | $6.00 | $0.25 | $2.00 | | `glm` | $1.40 | $4.40 | $0.26 | $1.40 | | `glm-5-3-flash` | $0.15 | $0.50 | $0.03 | $0.15 | | `muse-spark` | $1.25 | $4.25 | $0.15 | $1.25 | | `muse-spark-1-2` | $1.25 | $4.25 | $0.15 | $1.25 | | `muse-spark-1-1` | $1.25 | $4.25 | $0.15 | $1.25 | | `grok` | $2.00 | $6.00 | $0.50 | $2.00 | | `grok-4-6` | $2.00 | $6.00 | $0.50 | $2.00 | | `grok-4-5` | $2.00 | $6.00 | $0.30 | $2.00 | | `embed-small` | $0.02 | n/a | n/a | n/a | | [Jev decisions](https://docs.mindshub.ai/inference/decisions.md): `jev`, `jev-1.13.0` | $0 (promotion) | $0 | n/a | n/a | Aliases are described in [Models](https://docs.mindshub.ai/inference/models.md). Jev also accepts `jev-latest`. Who can call Jev without a daily cap is under [Jev decisions](#jev-decisions). `deepseek-v4-1-flash` is the fixed alias for the model `deepseek-v4-flash` serves today, so it prices identically to it and is not listed separately above. #### Jev decisions Jev input and output are priced at $0 during its launch promotion, and Jev requests never draw the included allowance. How much you can call it depends on how your organization is funded: - **With wallet credit, or with a card that has completed at least one top-up and has no unresolved payment error,** your organization calls Jev without a daily cap. - **Otherwise,** your organization gets a free daily allowance of 100,000 tokens and 100 requests, while shared free capacity lasts. The allowance refills continuously rather than resetting once a day: it adds that amount back over every 24 hours and never holds more than one day's worth, so a new organization, which starts with a full allowance, can use roughly twice that on its first day. A call that starts while the allowance has requests and tokens left runs to completion even if it uses more tokens than remain, and the overrun is taken from the refill that follows. Every call that passes the funding check counts as one request, including one the provider refuses or times out. A card on its own does not lift the cap; a completed top-up does. Past the allowance, or when shared free capacity runs out first, [`POST /v1/decisions`](https://docs.mindshub.ai/inference/decisions.md) returns `402 wallet_empty` before the model runs. Without credit, the call works again once the allowance refills. The `X-MindsHub-Recovery-Url` header carries the console path for adding credit, and credit lifts the cap within about a minute of landing. Organization access restrictions and [rate limits](https://docs.mindshub.ai/inference/rate-limits.md) apply to every organization. Usage is measured and returned on every call, and decision requests have no cache charges. #### Long-context pricing Some providers charge more for large prompts, and MindsHub passes those thresholds through. When the **total prompt** (uncached, cached, and cache-write tokens combined) exceeds the model's threshold, the higher rates apply to the **entire request**, on every dimension: | Model | Threshold (prompt tokens) | Input / Output | Cached input | Cache write | |---|---|---|---|---| | `mindshub_air` | above 272,000 | $0.20 / $0.75 | $0.02 | $0.25 | | `gpt` | above 272,000 | $20.00 / $75.00 | $2.00 | $25.00 | | `gpt-5-6-sol` | above 272,000 | $10.00 / $45.00 | $1.00 | $12.50 | | `gpt-sol` | above 272,000 | $4.00 / $15.00 | $0.20 | $5.00 | | `gpt-sol-6` | above 272,000 | $4.00 / $15.00 | $0.40 | $5.00 | | `gpt-terra` | above 272,000 | $4.00 / $18.00 | $0.40 | $5.00 | | `gpt-luna` | above 272,000 | $0.20 / $0.75 | $0.02 | $0.25 | | `gpt-luna-5-6` | above 272,000 | $0.40 / $1.80 | $0.04 | $0.50 | | `gemini` | above 200,000 | $4.00 / $18.00 | $0.40 | n/a | | `grok` | at or above 200,000 | $4.00 / $12.00 | $1.00 | $4.00 | | `grok-4-6` | at or above 200,000 | $4.00 / $12.00 | $1.00 | $4.00 | | `grok-4-5` | at or above 200,000 | $4.00 / $12.00 | $0.60 | $4.00 | Note the cached rates double too: a long, heavily-cached conversation is exactly the workload that crosses these thresholds. All other models price flat across their full context window. #### Web search and fetch Per 1,000 uses, on models where [web tools](https://docs.mindshub.ai/inference/web-search.md) are available. `mindshub_blaze`, `gpt-oss-fireworks` and `embed-small` are not among them: web tools are dropped on those aliases, so no search or fetch charge can arise on them no matter what your `tools` array says. On `mindshub_air` a search draws the included allowance first and bills at the rate below only past it. | Models | Search | Fetch | |---|---|---| | `mindshub_air`, `sonnet`, `sonnet-5`, `opus`, `opus-5`, `fable`, `fable-5`, `haiku`, `gpt`, `gpt-5-6-sol`, `gpt-sol`, `gpt-sol-6`, `gpt-terra`, `gpt-luna`, `gpt-luna-5-6`, `gpt-codex`, `gpt-mini`, `gpt-nano` | $10.00 | n/a | | `gemini`, `gemini-flash`, `gemini-flash-3-7`, `gemini-flash-3-6`, `gemini-flash-3-5`, `gemini-flash-3`, `gemini-flash-lite` | $14.00 | n/a | | `kimi`, `deepseek-v4-flash`, `qwen`, `glm`, `glm-5-3-flash`, `muse-spark`, `muse-spark-1-2`, `muse-spark-1-1` | $7.00 | $1.00 | | `grok`, `grok-4-6`, `grok-4-5` | $5.00 | n/a | Where no fetch price is listed, fetched pages bill as ordinary input tokens rather than per fetch. #### Remote MCP No per-call charge. The MCP server is yours and neither upstream charges to call it; what you pay is the tokens the server's tool schemas and results add to the turn, which `usage` reports as ordinary input and output tokens. Calls are counted (`mcp_call`) on your usage summary so the count is visible; the count carries no price today. See [Remote MCP servers](https://docs.mindshub.ai/inference/tool-calling.md#remote-mcp-servers). ### Checking usage and balance from code Two account endpoints accept your API key. They live on **`auth.mindshub.ai`** (not `api.mindshub.ai`), and the trailing slash is part of the path. **`GET https://auth.mindshub.ai/v1/entitlements/me/`**: what you can use right now: ```json { "wallet": {"balance_usd": "90.45", "can_consume": true, "auto_recharge": {"enabled": true, "...": "..."}}, "is_billing_owner": true, "models": [ {"id": "mindshub_air", "label": "MindsHub Air", "free_bucket": true, "embedding": false, "enabled": true, "reasoning_efforts": null, "default_reasoning_effort": "medium"} ], "included_percent_remaining": 42.5, "next_refresh_at": "2026-08-25T18:20:26Z" } ``` `next_refresh_at` is when the allowance refills. `free_bucket` marks the model it covers. `included_percent_remaining` is canonical and keeps fractional values, so 0.4% does not display as exhausted. The response also carries a deprecated `included_tokens` object for older clients, omitted from the example above and scheduled for removal. On a live allowance it restates the same proportion out of a `limit` of 100 and says so in a `unit` field; an uncapped allowance reports every counter as null, and an organization with no grant reports a `limit` of 0. Read `included_percent_remaining` instead. The absolute size of the allowance is not published. **`GET https://auth.mindshub.ai/v1/usage/summary/?range=period&group_by=model`**: what you've consumed. Response (excerpt; some fields omitted for brevity): ```json { "range": {"requested": "period", "start": "2026-07-01T00:00:00Z", "end": "2026-08-01T00:00:00Z"}, "results": [ {"dimensions": {"model_alias": "opus"}, "usage": {"input_tokens": 218809, "output_tokens": 226135, "billable_input_tokens": 218809, "billable_output_tokens": 226135, "search_count": 87, "fetch_count": 16}, "cost_usd": "19.54"} ], "next_cursor": "eyJvIjogM30=", "totals": {"usage": {"...": "..."}, "cost_usd": "19.54"} } ``` Reading it correctly: - **This endpoint reports paid consumption only.** `range=period` is the monthly paid billing period. The allowance runs on a separate fixed-duration window and is not reported here at all. Read it from `GET /v1/entitlements/me/`, where `included_percent_remaining` gives the proportion left and `next_refresh_at` gives the instant the window refills. - `cost_usd` is a decimal **string**, populated only for the billing owner (`null` otherwise), rounded to cents, so small usage reads `"0.00"`. Mid-period figures come from the in-progress invoice and can still move. There is no per-request cost anywhere in the API. - Results are paginated: pass `limit` (default 50, max 200) and follow `next_cursor`. - Rows break out cached-input, cache-write, and long-context tokens alongside input, output, search, and fetch, each with a billable twin. ### Summary of what bills where | Spend | Draws the allowance? | Bills wallet? | |---|---|---| | Weighted token lanes on `mindshub_air` (allowance remaining) | yes | no | | Tokens on `mindshub_air` (allowance exhausted) | no | yes | | Tokens on any other model | no | yes | | Cache writes on `mindshub_air` | yes, at weight 12 | only past the allowance | | Web search on `mindshub_air` | yes, 65,000 units per search | only past the allowance | | Web search on any other model, and fetch | never | yes | | Embeddings | never | yes | | Jev decisions | never | no, priced $0; without credit or a topped-up card free of payment errors, capped daily ([Jev decisions](#jev-decisions)) | | `count_tokens`, `GET /v1/models`, refused requests | no | no | --- ## Bring your own keys > **Status: planned.** BYOK is on the roadmap but not yet available. Nothing on this page works today; it describes what's coming so you can plan for it. Details may change before release. Today, every request runs on MindsHub's provider accounts and bills as described in [Billing](https://docs.mindshub.ai/inference/billing.md). BYOK will let you attach your own provider API keys (OpenAI, Anthropic, Gemini, and others) so that requests to those providers run on your account and contracts instead. ### What it will look like - **Add keys in settings.** One key per provider, validated with a live call when you save it. Keys are write-only: stored encrypted, never displayed again. You'll see the provider, a masked hint, and a validation status. - **Your key wins.** Once you've added, say, an Anthropic key, every request you make that resolves to an Anthropic-backed model runs on your key. Same aliases, same routing; only the account behind them changes. - **Billing follows the key.** Requests served on your key don't consume your MindsHub included allowance and don't draw your wallet. They're still metered for visibility and still subject to [rate limits](https://docs.mindshub.ai/inference/rate-limits.md). - **You choose what happens on providers you haven't keyed.** The default is "fill the gaps": those models keep working on MindsHub's keys, billed normally. The alternative is "my keys only": models on un-keyed providers show as unavailable. Use that mode if inference must never run on an account you don't control. - **Provider errors surface directly.** If your key is rejected or out of credit upstream, the error says so with a distinct code rather than presenting as a MindsHub failure. ### Provider keys only, not subscriptions BYOK is for **provider API keys**: the kind you get from an OpenAI or Anthropic developer console, with per-token billing attached. **Consumer subscriptions can't be attached.** A Claude Pro/Max or ChatGPT Plus subscription authenticates through OAuth tokens scoped to the vendor's own applications, not through developer API keys. ### In the meantime If your requirement is contractual, meaning inference must run through your own provider account, contact [support@mindsdb.com](mailto:support@mindsdb.com); it helps us prioritize. If your need is capacity, [wallet billing](https://docs.mindshub.ai/inference/billing.md) has no usage ceiling beyond the [rate limits](https://docs.mindshub.ai/inference/rate-limits.md). --- ## Capability matrix Every table on this page is generated from the code that serves requests and from the production model policy, so it cannot say something the router doesn't do. Where a cell says a parameter is dropped, the response header `X-MindsHub-Dropped-Params` will name it at request time; where it says clamped, `X-MindsHub-Clamped-Params` will (except on a stream whose `200` went out early; see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers)). The rule behind both is in [Core concepts](https://docs.mindshub.ai/inference/concepts.md#parameters-are-adapted-not-rejected). ### Legend | Cell | Meaning | |---|---| | yes | Honored on every model behind this API. | | per model | Honored where the target model takes it, dropped and named in `X-MindsHub-Dropped-Params` where it doesn't. The models table says which. | | partial | Honored with a limitation stated in the row's notes. | | no | Accepted and ignored. The request succeeds and the feature silently doesn't happen. | | 400 | Refused with a 400 naming the field, because serving the request would answer a different question than the one asked. | | no field | This API's wire format has no field for it. | | forwarded / adapted / conditional / ignored / rejected | The parameter table's own vocabulary: relayed as sent; honored with translation; honored per model; accepted and ignored; refused with a 400. | ### Features by API One row per feature, one column per request format. Spellings differ by API; the semantics are the same because all three reach the same service. | Feature | Chat Completions | Responses | Messages | Notes | |---|---|---|---|---| | Streaming | yes · `stream` | yes · `stream` | yes · `stream` | | | Tool calling | yes · `tools` | yes · `tools` | yes · `tools` | | | Parallel tool calls | per model · `parallel_tool_calls` | per model · `parallel_tool_calls` | per model · `tool_choice.disable_parallel_tool_use` | | | Structured output (JSON schema) | per model · `response_format` | per model · `text.format` | per model · `output_config.format` | The one parameter refused rather than dropped when the model can't honor it. | | Schema-less JSON mode | partial | partial | no field | `json_object` is a 400 on the Claude family, which has no schema-less mode. | | Reasoning effort | per model · `reasoning_effort` | per model · `reasoning.effort` | per model · `output_config.effort` | | | Extended thinking object | no field | no field | per model · `thinking` | | | Reasoning carried across turns | no field | per model · `include: reasoning.encrypted_content` | no field | | | Web search | yes | yes | yes | Native or external per model; the models table says which. On a model without search the web tools are dropped. | | Images in | yes | yes | yes | Accepted on every model; a model without vision returns its provider's own error. | | PDF / file input | partial | no | no | A Chat Completions `file` part reaches OpenAI-transport models only; Responses `input_file` parts and Anthropic Files API references are dropped. | | Automatic prompt caching | yes | yes | yes | | | Explicit cache breakpoints | no field | no field | partial | Block-level `cache_control` is honored on Claude-family targets; the top-level auto-caching switch is ignored. | | Server-side conversation state | no · `store` | yes · `store` | no field | `store` and `previous_response_id`; 30-day retention, chains walked 25 turns back. | | Stored-response operations | no field | yes | no field | `GET`/`DELETE /v1/responses/{id}`, `input_items`, `cancel`. | | Token counting endpoint | no field | no field | yes | `POST /v1/messages/count_tokens`, free and unmetered. | | Legacy `functions` / `function_call` | yes · `functions` | no field | no field | | | `web_search_options` | yes · `web_search_options` | no field | no field | | | Sampling (`temperature`, `top_p`) | per model · `temperature` | per model · `temperature` | per model · `temperature` | | | `seed` and repetition penalties | per model · `seed` | no field | no field | | | `verbosity` | per model · `verbosity` | per model · `text.verbosity` | no field | | | Conversations API (`conversation`) | no field | 400 · `conversation` | no field | | ### Request parameters by API One row per parameter concept. Each cell shows how that API treats it (forwarded, adapted, conditional on the model, ignored, or rejected with a 400) and the spelling that API uses. The same tables, filtered to one API, appear on each [API reference](https://docs.mindshub.ai/inference/chat-completions.md#request-parameters) page. | Group | Parameter | Chat Completions | Responses | Messages | |---|---|---|---|---| | Structure | `model` | adapted · `model` | adapted · `model` | adapted · `model` | | Structure | conversation so far | adapted · `messages` | adapted · `input` | conditional · `messages` | | Structure | system prompt | adapted · `messages[role=system]` | adapted · `instructions` | conditional · `system` | | Structure | `stream` | forwarded · `stream` | forwarded · `stream` | forwarded · `stream` | | Structure | `stream_options` | adapted · `stream_options` | ignored · `stream_options` | no field | | Generation | `max_tokens` | conditional · `max_tokens` | conditional · `max_output_tokens` | conditional · `max_tokens` | | Generation | `max_completion_tokens` | adapted · `max_completion_tokens` | no field | no field | | Generation | `temperature` | conditional · `temperature` | conditional · `temperature` | conditional · `temperature` | | Generation | `top_p` | conditional · `top_p` | conditional · `top_p` | conditional · `top_p` | | Generation | `top_k` | conditional · `top_k` | no field | conditional · `top_k` | | Generation | `stop_sequences` | conditional · `stop` | no field | conditional · `stop_sequences` | | Generation | `seed` | conditional · `seed` | no field | no field | | Generation | `presence_penalty` | conditional · `presence_penalty` | no field | no field | | Generation | `frequency_penalty` | conditional · `frequency_penalty` | no field | no field | | Generation | `verbosity` | conditional · `verbosity` | conditional · `text.verbosity` | no field | | Reasoning | `reasoning_effort` | conditional · `reasoning_effort` | conditional · `reasoning.effort` | adapted · `output_config.effort` | | Reasoning | `thinking` | no field | no field | conditional · `thinking` | | Reasoning | `include` | no field | conditional · `include` | no field | | Tools | `tools` | adapted · `tools` | adapted · `tools` | adapted · `tools` | | Tools | `tool_choice` | adapted · `tool_choice` | adapted · `tool_choice` | adapted · `tool_choice` | | Tools | `parallel_tool_calls` | conditional · `parallel_tool_calls` | conditional · `parallel_tool_calls` | adapted · `tool_choice.disable_parallel_tool_use` | | Tools | `web_search_options` | adapted · `web_search_options` | no field | no field | | Tools | `mcp_servers` | no field | no field | adapted · `mcp_servers` | | Tools | `functions` | adapted · `functions` | no field | no field | | Tools | `function_call` | adapted · `function_call` | no field | no field | | Output shape | `response_format` | conditional · `response_format` | conditional · `text.format` | adapted · `output_config.format` | | Storage and chaining | `store` | ignored · `store` | adapted · `store` | no field | | Storage and chaining | `previous_response_id` | no field | adapted · `previous_response_id` | no field | | Storage and chaining | `background` | no field | ignored · `background` | no field | | Accepted and ignored | `metadata` | ignored · `metadata` | ignored · `metadata` | ignored · `metadata` | | Accepted and ignored | `user` | ignored · `user` | ignored · `user` | no field | | Accepted and ignored | `safety_identifier` | ignored · `safety_identifier` | ignored · `safety_identifier` | no field | | Accepted and ignored | `service_tier` | ignored · `service_tier` | ignored · `service_tier` | ignored · `service_tier` | | Accepted and ignored | `prompt_cache_key` | ignored · `prompt_cache_key` | ignored · `prompt_cache_key` | no field | | Accepted and ignored | `prompt_cache_retention` | ignored · `prompt_cache_retention` | ignored · `prompt_cache_retention` | no field | | Accepted and ignored | `n` | ignored · `n` | no field | no field | | Accepted and ignored | `logprobs` | ignored · `logprobs` | no field | no field | | Accepted and ignored | `top_logprobs` | ignored · `top_logprobs` | ignored · `top_logprobs` | no field | | Accepted and ignored | `logit_bias` | ignored · `logit_bias` | no field | no field | | Accepted and ignored | `prediction` | ignored · `prediction` | no field | no field | | Accepted and ignored | `modalities` | ignored · `modalities` | no field | no field | | Accepted and ignored | `audio` | ignored · `audio` | no field | no field | | Accepted and ignored | `truncation` | no field | ignored · `truncation` | no field | | Accepted and ignored | `max_tool_calls` | no field | ignored · `max_tool_calls` | no field | | Accepted and ignored | cache_control (top-level) | no field | no field | ignored · `cache_control` | | Accepted and ignored | `container` | no field | no field | ignored · `container` | | Accepted and ignored | `inference_geo` | no field | no field | ignored · `inference_geo` | | Refused | `conversation` | no field | rejected (400) · `conversation` | no field | | Refused | `prompt` | no field | rejected (400) · `prompt` | no field | | Refused | `context_management` | no field | rejected (400) · `context_management` | no field | ### Providers Transport-level facts about each upstream provider. The models table below already has these intersected in; this table explains *why* a model row reads the way it does. | Provider | Transport | Native web search | External search loop | Remote MCP | MCP approvals | MCP deny-list | Named `tool_choice` | `tool_choice: none` | Params the transport can't carry | `max_tokens` floor | Notes | |---|---|---|---|---|---|---|---|---|---|---|---| | `openai` | `openai_responses` | `openai_native` | no | native | yes | no | yes | yes | `top_k`, `stop_sequences`, `thinking`, `seed`, `presence_penalty`, `frequency_penalty` | 16 | Real api.openai.com rejects `max_tokens` below 16; requests below the floor are raised to it. | | `anthropic` | `anthropic_messages` | `anthropic_native` | no | native | no | yes | yes | yes | `seed`, `presence_penalty`, `frequency_penalty`, `verbosity` | none | | | `gemini` | `gemini_native` | `gemini_google_search` | no | our client | no | yes | yes | yes | `thinking`, `parallel_tool_calls`, `verbosity` | none | Sets thinking depth through `thinking_level`, which the effort parameter carries; the `fetch` web tool is always dropped. | | `fireworks` | `fireworks` | no | yes | our client | no | yes | yes | no | `thinking`, `seed`, `presence_penalty`, `frequency_penalty`, `verbosity` | none | Shares Anthropic's wire shape but not its backend: `tool_choice: none` is honored by dropping the tools. | | `moonshot` | `moonshot` | `moonshot_native` | yes | our client | no | yes | no | yes | `thinking`, `reasoning_effort`, `verbosity` | none | Kimi reasons internally with no adjustable level. Its own `$web_search` builtin or the external loop, per policy. | | `meta` | `fireworks` | no | yes | our client | no | yes | yes | no | `thinking`, `seed`, `presence_penalty`, `frequency_penalty`, `verbosity` | none | As Fireworks. `response_format` is emulated as a synthetic tool call rather than sent natively, forced where the alias allows it and offered unforced (best effort) where it doesn't, pending verification of Meta's own spelling. | | `cerebras` | `moonshot` | no | yes | our client | no | yes | yes | yes | `thinking`, `top_k`, `verbosity` | none | | | `groq` | `moonshot` | no | yes | our client | no | yes | yes | yes | `thinking`, `top_k`, `verbosity` | none | | | `xai` | `openai_responses` | `openai_native` | no | native | no | no | yes | yes | `top_k`, `stop_sequences`, `thinking`, `seed`, `presence_penalty`, `frequency_penalty`, `verbosity` | none | OpenAI-compatible Responses transport; `verbosity` unverified and dropped. | ### Request structure per transport What a request's *shape* may say on each wire, as opposed to what its parameters may say. A client can send a content block or a tool-schema keyword the target has no field for; where that happens it is dropped rather than forwarded into a 400, and named on `X-MindsHub-Dropped-Content`. Two transports forward what they do not recognize instead, and the reason is published here rather than left to be inferred. | Transport | Providers | Content blocks it carries | A block type it does not declare | Tool schema | |---|---|---|---|---| | `anthropic_messages` | `anthropic` | `document`, `image`, `image_url`, `redacted_thinking`, `search_result`, `server_tool_use`, `text`, `thinking`, `tool_result`, `tool_use`, `web_search_tool_result` | forwarded — the target speaks Anthropic natively, so a block type this build has not enumerated is far more likely to be a new Anthropic block than a foreign one, and dropping it would break a request the upstream serves perfectly well | `json_schema` | | `fireworks` | `fireworks`, `meta` | `image`, `image_url`, `text`, `tool_result`, `tool_use` | dropped and reported | `json_schema` | | `gemini_native` | `gemini` | `image_url`, `text` | dropped and reported | `gemini_openapi` | | `moonshot` | `cerebras`, `groq`, `moonshot` | `image_url`, `text` | dropped and reported | `json_schema` | | `openai_responses` | `openai`, `xai` | `computer_screenshot`, `encrypted_content`, `file`, `image`, `image_url`, `input_audio`, `input_file`, `input_image`, `input_text`, `output_text`, `refusal`, `summary_text`, `text` | forwarded — this transport carries every backend speaking an OpenAI-compatible Responses API, not just real OpenAI — xAI reuses it unchanged — so a type outside the allowlist may be exactly right for whichever backend the request is routed to, and this gateway cannot tell | `json_schema` | ### Models One row per generation alias, in the order `GET /v1/models` lists them. Pinned versions share their family's facts. Embedding aliases are on [Embeddings](https://docs.mindshub.ai/inference/embeddings.md). | Alias | Provider | Effort levels | `thinking` object | Web search | Named `tool_choice` | `tool_choice: none` | Parallel tool calls | JSON schema | JSON mode | Sampling | Stop sequences | `seed` and penalties | `verbosity` | Max output tokens | Context window | Notes | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | `mindshub_air` | `openai` | not adjustable | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `mindshub_blaze` | `cerebras` | `none` · `high` | no | none | yes | yes | yes | yes | yes | `temperature`, `top_p` | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | provider default | 131,072 | | | `gpt-oss-fireworks` | `fireworks` | not adjustable | no | none | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | 131,072 | | | `sonnet` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | → auto | drops tools | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `sonnet-5-5` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | → auto | drops tools | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `sonnet-5` | `anthropic` | `low` · `medium` · `high` · `max` | yes | native | yes | yes | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `opus` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | → auto | drops tools | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `opus-5-5` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | → auto | drops tools | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `opus-5` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | yes | yes | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `fable` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | → auto | drops tools | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `fable-5-1` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | → auto | drops tools | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `fable-5` | `anthropic` | `low` · `medium` · `high` · `xhigh` · `max` | yes | native | yes | yes | yes | yes | 400 | none | yes | none | no | 128,000 | 1,000,000 | | | `haiku` | `anthropic` | not adjustable | yes | native | yes | yes | yes | yes | 400 | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | 200,000 | [b] | | `haiku-4-5` | `anthropic` | not adjustable | yes | native | yes | yes | yes | yes | 400 | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | 200,000 | [b] | | `gpt` | `openai` | `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-6-astra` | `openai` | `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-5-6-sol` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-sol` | `openai` | `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-sol-6` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-sol-6-1` | `openai` | `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-terra` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-terra-5-6` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-luna` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-luna-5-6` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-luna-6` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` · `max` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 1,050,000 | [a] | | `gpt-codex` | `openai` | `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 400,000 | [a] | | `gpt-codex-5-3` | `openai` | `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | none | no | none | yes | 128,000 | 400,000 | [a] | | `gpt-mini` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | yes | provider default | 400,000 | [a] | | `gpt-mini-5-4` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | yes | provider default | 400,000 | [a] | | `gpt-nano` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | yes | provider default | 400,000 | [a] | | `gpt-nano-5-4` | `openai` | `none` · `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | yes | provider default | 400,000 | [a] | | `gemini` | `gemini` | `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-3-1-pro` | `gemini` | `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-flash` | `gemini` | `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-flash-3-8` | `gemini` | `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-flash-3-7` | `gemini` | `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-flash-3-6` | `gemini` | `minimal` · `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-flash-3-5` | `gemini` | `minimal` · `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-flash-3` | `gemini` | `minimal` · `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | `temperature`, `top_p`, `top_k` | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | provider default | 1,048,576 | | | `gemini-flash-lite` | `gemini` | `minimal` · `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `gemini-flash-lite-3-1` | `gemini` | `minimal` · `low` · `medium` · `high` | no | native | yes | yes | no | yes | yes | none | yes | `seed`, `presence_penalty`, `frequency_penalty` | no | 65,536 | 1,048,576 | | | `kimi` | `fireworks` | not adjustable | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | [c] | | `kimi-k3` | `fireworks` | not adjustable | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `deepseek-v4-flash` | `fireworks` | `none` · `low` · `high` · `max` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `deepseek-v4-1-flash` | `fireworks` | `none` · `low` · `high` · `max` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `qwen` | `fireworks` | `low` · `medium` · `xhigh` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `qwen-3-8-a95b` | `fireworks` | `low` · `medium` · `xhigh` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `glm` | `fireworks` | `low` · `high` · `max` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `glm-5-3` | `fireworks` | `low` · `high` · `max` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `glm-5-3-flash` | `fireworks` | `low` · `high` · `max` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `glm-flash-5-3` | `fireworks` | `low` · `high` · `max` | no | external (exa) | yes | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | — | | | `muse-spark` | `meta` | not adjustable | no | external (exa) | → auto | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | 1,048,576 | [d] | | `muse-spark-1-3` | `meta` | not adjustable | no | external (exa) | → auto | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | 1,048,576 | | | `muse-spark-1-2` | `meta` | not adjustable | no | external (exa) | → auto | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | 1,048,576 | | | `muse-spark-1-1` | `meta` | not adjustable | no | external (exa) | → auto | drops tools | yes | yes | yes | `temperature`, `top_p`, `top_k` | yes | none | no | provider default | 1,048,576 | | | `grok` | `xai` | `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | no | provider default | 500,000 | | | `grok-4-7` | `xai` | `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | no | provider default | 500,000 | | | `grok-4-6` | `xai` | `low` · `medium` · `high` · `xhigh` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | no | provider default | 500,000 | | | `grok-4-5` | `xai` | `low` · `medium` · `high` | no | native | yes | yes | yes | yes | yes | `temperature`, `top_p` | no | none | no | provider default | 500,000 | | - [a] `max_tokens` below 16 is raised to 16. - [b] Takes `temperature` or `top_p`, not both: when both are sent, `top_p` is dropped and named in `X-MindsHub-Dropped-Params`. - [c] Takes `temperature` only at `1` and `top_p` only at `0.95`; other values return the provider's own 400. Tool schemas are normalized for Kimi's stricter validator. - [d] Only `tool_choice: "auto"` is accepted upstream; named and `required` are rewritten to `auto`, `none` is honored by dropping the tools. `response_format` is emulated as an unforced synthetic tool call rather than sent natively (this alias cannot force one), pending verification of Meta's own spelling — best effort, not guaranteed — see [Structured output](https://docs.mindshub.ai/inference/structured-output.md). No production alias currently routes to: `moonshot`, `groq`. Generated from the live production model policy on 2026-10-08. --- ## Chat Completions `POST /v1/chat/completions` follows the OpenAI chat completions shape: send a `model` and a list of `messages`, get back a completion. It is the most widely supported request format in the ecosystem and the one to start with. The OpenAI SDKs work against it with only a `base_url` change, and **any model in the [catalog](https://docs.mindshub.ai/inference/models.md) can serve it**. ### Endpoint `POST https://api.mindshub.ai/v1/chat/completions` OpenAI SDKs take the base URL **with** `/v1` and authenticate with `api_key`. Install the official client: `pip install openai` or `npm install openai`. ### Quick example ```python Python from openai import OpenAI import os client = OpenAI( base_url="https://api.mindshub.ai/v1", api_key=os.environ["MINDSHUB_API_KEY"], ) response = client.chat.completions.create( model="mindshub_air", messages=[ {"role": "system", "content": "You are a terse assistant."}, {"role": "user", "content": "What is the capital of Australia?"}, ], ) print(response.choices[0].message.content) ``` ```typescript TypeScript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.mindshub.ai/v1", apiKey: process.env.MINDSHUB_API_KEY, }); const response = await client.chat.completions.create({ model: "mindshub_air", messages: [ { role: "system", content: "You are a terse assistant." }, { role: "user", content: "What is the capital of Australia?" }, ], }); console.log(response.choices[0].message.content); ``` The samples call `mindshub_air`, the alias your recurring [included allowance](https://docs.mindshub.ai/inference/billing.md) covers, so they work on an account with no payment method on file. Change the `model` string to any alias in [Models](https://docs.mindshub.ai/inference/models.md) once you have a wallet balance; nothing else about the request changes. The TypeScript sample uses top-level `await`, so run it as an ES module. ### Request parameters | Parameter | Handling | Notes | |---|---|---| | `model` | adapted | An alias from GET /v1/models resolves to a concrete provider model. The response names the model that served, which can be sent back as-is (it resolves to the alias serving that model, not to a pin); `mindshub_air` and `mindshub_blaze` return their own alias instead. The deprecated `latest:` spelling still resolves. | | `messages` | adapted | system/user/assistant/tool all round-trip, and `developer` is read as `system`. Text and image_url content parts translate to every provider; `file` and `input_audio` parts do not (Milestone 4). `name` survives only to Moonshot, and as the tool-name fallback on Gemini. `hosted_tool_calls` is an internal carrier for the other two lanes' replayed remote-MCP records and is ignored here. | | `stream` | forwarded | SSE `chat.completion.chunk` frames terminated by `[DONE]`. Null-valued keys are omitted rather than sent as null, so non-terminal chunks carry no `finish_reason` key. | | `stream_options` | adapted | `include_usage` is honoured; other keys are ignored. Never reaches a provider — it shapes our own response — so no model can fail to support it. Kimi streams end with a usage chunk whether or not it was asked for. | | `max_tokens` | conditional | Clamped down to the resolved model's output ceiling and up to the transport's floor; either is named in X-MindsHub-Clamped-Params. Above the 131,072 admission ceiling the request is refused with `max_tokens_exceeded` before a provider is touched. | | `max_completion_tokens` | adapted | OpenAI's newer spelling of max_tokens and wins over it when both are set to positive values. A value of 0 is falsy here and falls through to max_tokens. | | `temperature` | conditional | Dropped on models that reject sampling params — every current Claude 5 model, Gemini 3.x, and the GPT 5.6/5.3-codex line — and named in X-MindsHub-Dropped-Params. | | `top_p` | conditional | As temperature. On a Claude model that still takes sampling params, sending it with temperature drops top_p (Anthropic rejects the pair) and names it in X-MindsHub-Dropped-Params; top_p alone is forwarded. | | `top_k` | conditional | A MindsHub extension, not an OpenAI parameter. Forwarded where the transport has it; the OpenAI Responses shape does not, so it is always dropped on the GPT and Grok families and named in X-MindsHub-Dropped-Params. | | `stop` | conditional | Forwarded where the transport has a stop-sequence field; the OpenAI Responses shape, which serves the GPT and Grok families here, does not. Reported under the internal name `stop_sequences`, not `stop`. | | `seed` | conditional | Reaches Gemini and Moonshot, the only two transports with a field for it. Never a reproducibility guarantee even there. | | `presence_penalty` | conditional | As seed. | | `frequency_penalty` | conditional | As seed. | | `verbosity` | conditional | Rides `text.verbosity` on the OpenAI Responses transport, the only one that has it; dropped and named everywhere else. | | `reasoning_effort` | conditional | Clamped onto the resolved model's published ladder rather than dropped — with no level on the request the provider applies its own, which sits near the top. An unrecognized level fits no rung, so it falls back to the model's published `default_reasoning_effort` clamped onto that ladder, and is reported as a clamp naming the category `other` rather than the value sent; it is dropped only where the model publishes no default. A model that publishes a default and an empty ladder is pinned: every request runs at that level and a level sent is replaced by it (`mindshub_air`). Kimi reasons internally with no adjustable level, so the transport declares it unsupported. | | `tools` | adapted | Function tools translate to every provider, `strict` included. The platform's own `web_search`/`fetch` entries map onto each provider's hosted search, or our external loop, and are dropped where the target has neither. An OpenAI-shaped `mcp` entry (MindsHub extension: Chat Completions itself has none) runs on the target's own remote-MCP connector; this lane cannot hand back an approval request, so only tools with `require_approval: "never"` run and the rest are named in X-MindsHub-Dropped-Params. The answer reflects the tool output; the body has no slot for the calls. | | `tool_choice` | adapted | auto/required/none/named all map. A forced choice a model cannot honour is downgraded to `auto` rather than refused (ENG-1095), and `none` is honoured by dropping the tools on transports with no word for it — both reported in X-MindsHub-Clamped-Params. | | `parallel_tool_calls` | conditional | Honoured, including on the Anthropic wire, which spells it inverted inside tool_choice. Gemini cannot express it and declares it unsupported, so the drop is named. | | `web_search_options` | adapted | Read as the platform's own `{'type': 'web_search'}` tool entry, so it enables hosted search on models that have it. `search_context_size` and `user_location` are dropped: one body here routes to seven backends whose search tools take different options. | | `functions` | adapted | OpenAI's pre-`tools` spelling, deprecated by OpenAI and still served by them. Honoured in all four places they switch: the request becomes `tools`, a replayed assistant.function_call and 'function' role become tool_calls and a 'tool' message, and the answer comes back as message.function_call with finish_reason 'function_call' on both the JSON and SSE paths. Only the first call of a multi-call turn is representable. See minds/requests/legacy_functions.py. | | `function_call` | adapted | Translated into `tool_choice`; an unrecognized string is dropped rather than forwarded into a provider 400. Sending `tools` alongside either legacy field is read as a modern request, and answered in the modern shape. | | `response_format` | conditional | json_schema and json_object both parse; each transport renders the schema in its own shape. The ONE param that raises rather than being dropped when a model cannot honour it (CONTRACT_PARAMS): 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 Anthropic wire, which has no schema-less JSON mode. | | `store` | ignored | Chat completions are not stored and there is no retrieval endpoint for them. `/v1/responses` does store, and defaults to it. | | `metadata` | ignored | Accepted and not read. Must be an object if present, so a string is a 400. | | `user` | ignored | Requests are attributed by API key and the headers the edge supplies. Not reported as a drop: nothing about the answer changes. | | `safety_identifier` | ignored | As user. | | `service_tier` | ignored | One serving tier; nothing to select. The response echoes `default`. | | `prompt_cache_key` | ignored | Cache routing is derived from the request's own stable prefix, which is more reliable than a key a caller has to keep consistent themselves. | | `prompt_cache_retention` | ignored | Retention is the upstream provider's. | | `n` | ignored | There is always exactly one choice. Reported in X-MindsHub-Dropped-Params unless it was 1, which is what we do. | | `logprobs` | ignored | No transport here returns log probabilities: they are Chat Completions fields, and the shape serving the GPT and Grok families is the Responses API. Reported as a drop unless explicitly false. | | `top_logprobs` | ignored | As logprobs. | | `logit_bias` | ignored | As logprobs. Reported as a drop. | | `prediction` | ignored | Predicted outputs are not offered. Reported as a drop. | | `modalities` | ignored | Text only; audio is Milestone 4. Reported as a drop. | | `audio` | ignored | As modalities. | *This table is generated from `PARAM_SUPPORT` in `minds/requests/chat_completions_request.py`, which a unit test holds to the vendor SDK's own parameter list. The cross-API view is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#request-parameters-by-api).* **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. 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; the rows marked `ignored` above say which fields those are, and which are not reported because they describe your account rather than the generation. The one exception is a stream whose `200` went out early, which carries no `X-MindsHub-*` header; see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers). The cross-API view of every parameter is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#request-parameters-by-api). #### 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. Supported part types are `text` and `image_url`; see [Images](https://docs.mindshub.ai/inference/images.md) for the image rules. ### Request body The full request and response schemas, with every field described, are on the generated reference page: [POST /v1/chat/completions](/api-reference/create-chat-completion). The spec itself is at [openapi.json](https://docs.mindshub.ai/inference/openapi.json). ### Response ```json { "id": "chatcmpl-636a4f9b-80c4-4989-a48d-0c9bed215ba7", "object": "chat.completion", "created": 1785401283, "model": "mindshub_air", "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` names the model that served the request, fallback included, and can be sent again as-is. `mindshub_air` and `mindshub_blaze` return their own alias instead. See the [models page](https://docs.mindshub.ai/inference/models.md#what-the-responses-model-field-contains). - 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 when the model refused without saying why. `finish_reason` is `content_filter` either way. - `usage.completion_tokens_details` is on every response. Its `reasoning_tokens` is a **subset** of `completion_tokens`, broken out by the GPT/Grok families and Gemini models; elsewhere it reads `0`, which means *no separately-reported reasoning* rather than *no reasoning*. See [Reasoning](https://docs.mindshub.ai/inference/reasoning.md). - `service_tier` is always `"default"`. - When the request touched a provider's prompt cache, `usage` gains `prompt_tokens_details`. See [Prompt caching](https://docs.mindshub.ai/inference/prompt-caching.md). - A turn that searched reports its sources on `message.annotations`. See [Web search](https://docs.mindshub.ai/inference/web-search.md). #### `finish_reason` | Value | Meaning | |---|---| | `stop` | The model finished normally. | | `length` | Output was truncated at `max_tokens` (or the model's context limit). | | `tool_calls` | The model stopped to call one or more tools. | | `content_filter` | The upstream provider refused to continue. | | `null` / absent | Abnormal 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-v4-flash`, `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 events `"stream": true` returns `text/event-stream`: `data: ` lines carrying `chat.completion.chunk` frames, terminated by `data: [DONE]`. Usage arrives in a final choice-less chunk when you send `stream_options: {"include_usage": true}`. The frame format, the differences from OpenAI's stream, and the failure rules are in [Streaming](https://docs.mindshub.ai/inference/streaming.md#chat-completions). ### Lane-specific behaviour - **Funding is checked before the model runs.** An empty wallet or exhausted allowance refuses the request up front (`402` or `429`) at no cost. See [Billing](https://docs.mindshub.ai/inference/billing.md). - **Failover is reported.** During a provider outage the platform may serve your request through a fallback model. `X-MindsHub-Failover`, `X-MindsHub-Attempts` and the body's `mindshub` field say so (a stream whose `200` went out early carries only the body field, and not even that if it fails), and `X-MindsHub-Allow-Failover: false` turns it off. See [Failover](https://docs.mindshub.ai/inference/models.md#failover). - **Legacy `functions`/`function_call`** are served in the legacy response shape. See [Tool calling](https://docs.mindshub.ai/inference/tool-calling.md#legacy-function-calling-chat-completions-only). ### Errors Errors use the OpenAI envelope: `{"error": {"message", "type", "param", "code"}}`, so `client.chat.completions.create(...)` raises the SDK's typed errors. A `400` from a malformed request body carries `param` naming the field. Status codes and their meanings are shared across the API; see [Errors](https://docs.mindshub.ai/inference/errors.md). *The per-parameter contract this page summarizes is maintained in code, as `PARAM_SUPPORT` in `minds/requests/chat_completions_request.py`, and held to the OpenAI SDK's own parameter list by a test.* --- ## Choosing an API Choose the kind of result first. Chat models generate content, embedding models produce vectors, and decision models return typed judgments. They use the same MindsHub key and usage reporting. For chat models, MindsHub supports three request formats: Chat Completions, Responses, and Messages. Choose whichever your existing code speaks. Jev uses the [Decisions API](https://docs.mindshub.ai/inference/decisions.md), with native `state` and typed `questions`. Model catalog entries with `kind: "decision"` use `/v1/decisions`; the chat APIs described here apply to `kind: "chat"`. ### Pick in ten seconds | If you… | Use | Base URL | Auth | |---|---|---|---| | Already call OpenAI's `chat.completions` | [Chat Completions](https://docs.mindshub.ai/inference/chat-completions.md) | `https://api.mindshub.ai/v1` | `api_key` | | Already call OpenAI's `responses` | [Responses](https://docs.mindshub.ai/inference/responses.md) | `https://api.mindshub.ai/v1` | `api_key` | | Already use the Anthropic SDK or Claude Code | [Messages](https://docs.mindshub.ai/inference/messages.md) | `https://api.mindshub.ai` | `auth_token` | | Need categories, scores, or yes/no probabilities | [Decisions (Jev)](https://docs.mindshub.ai/inference/decisions.md) | `https://api.mindshub.ai/v1/decisions` (full endpoint) | Bearer key over HTTP | | Need text vectors | [Embeddings](https://docs.mindshub.ai/inference/embeddings.md) | `https://api.mindshub.ai/v1` | `api_key` | | Are starting fresh with a chat model | [Chat Completions](https://docs.mindshub.ai/inference/chat-completions.md) | `https://api.mindshub.ai/v1` | `api_key` | | Run OpenAI Codex | [Responses](https://docs.mindshub.ai/inference/responses.md) | `https://api.mindshub.ai/v1` | `api_key` | **Starting a chat integration?** Use Chat Completions. It is the most widely supported shape in the ecosystem and the best documented here. The two URL traps, worth repeating because they cause most first-run failures: - OpenAI SDKs want the base URL **with** `/v1`. Anthropic SDKs want it **without**; the client appends `/v1/messages` itself. - Anthropic SDKs must authenticate with **`auth_token`**, not `api_key`. `api_key` sends an `x-api-key` header, which MindsHub rejects with a 401 before the request lands. ### Capability at a glance Every **chat** model in the [catalog](https://docs.mindshub.ai/inference/models.md) is reachable from all three chat APIs below. What differs is a handful of protocol features. The generated, per-parameter and per-model version of this table is the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md); each row below links to the guide that shows the feature on all three APIs. | Capability | Chat Completions | Responses | Messages | |---|---|---|---| | Every generation model | yes | yes | yes | | [Streaming](https://docs.mindshub.ai/inference/streaming.md) | yes | yes | yes | | [Tool calling](https://docs.mindshub.ai/inference/tool-calling.md) | yes | yes | yes | | [Images](https://docs.mindshub.ai/inference/images.md) in | yes | yes | yes | | [Built-in web search](https://docs.mindshub.ai/inference/web-search.md) | yes | yes | yes | | [Reasoning effort](https://docs.mindshub.ai/inference/reasoning.md) | `reasoning_effort` | `reasoning.effort` | `output_config.effort` | | [Automatic prompt caching](https://docs.mindshub.ai/inference/prompt-caching.md) | yes | yes | yes | | Explicit cache breakpoints | no | no | `cache_control` | | Token counting endpoint | no | no | `/v1/messages/count_tokens` | | [Conversation chaining](https://docs.mindshub.ai/inference/conversation-state.md) | no | `previous_response_id` | no | | [Guaranteed JSON / schema output](https://docs.mindshub.ai/inference/structured-output.md) | `response_format` | `text.format` | `output_config.format` | Embedding models use [Embeddings](https://docs.mindshub.ai/inference/embeddings.md). Decision models such as Jev use [Decisions](https://docs.mindshub.ai/inference/decisions.md); learn how their question types work in [Decision models](https://docs.mindshub.ai/inference/decision-models.md). Two of those rows need explanation: **Conversation chaining is Responses-only.** On Chat Completions and Messages, send the full conversation each turn and keep history client-side. Responses additionally stores turns (`store` defaults to `true`) and chains them by `previous_response_id`; see [Conversation state](https://docs.mindshub.ai/inference/conversation-state.md). Sending the full history each turn works on all three. **Structured output works the same on all three.** Each API spells it differently — `response_format`, the Responses `text` field, Messages' `output_config` — and each is honored, so a JSON schema comes back as JSON matching it. See [Structured output](https://docs.mindshub.ai/inference/structured-output.md). ### Parameter handling Whichever API you pick, parameters the target model supports are honored at whatever value you send; ones it doesn't support are dropped, and out-of-range values are clamped, with every change reported in the `X-MindsHub-Dropped-Params` and `X-MindsHub-Clamped-Params` response headers (except on a stream whose `200` went out early; see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers)). A model can still restrict the values of a parameter it supports and return its own 400 (Kimi K3 takes `temperature` only at `1`). A few models refuse two supported parameters sent together, and one of them is dropped: Claude Haiku 4.5 takes `temperature` or `top_p`, so `top_p` goes when both are sent. Unknown top-level fields are accepted and ignored on all three APIs; unknown *nested* content can still reach the provider and fail there. The full contract is in [Core concepts](https://docs.mindshub.ai/inference/concepts.md#parameters-are-adapted-not-rejected). ### Response shapes differ The three APIs return their own native shapes, which is the point: your existing parsing code keeps working. ```json Chat Completions { "id": "chatcmpl-636a4f9b", "object": "chat.completion", "model": "sonnet", "choices": [{ "index": 0, "message": {"role": "assistant", "content": "Canberra."}, "finish_reason": "stop" }], "usage": {"prompt_tokens": 12, "completion_tokens": 4, "total_tokens": 16} } ``` ```json Responses { "id": "resp_9f2c1a", "object": "response", "status": "completed", "model": "sonnet", "output": [{ "type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Canberra.", "annotations": []}] }], "output_text": "Canberra.", "usage": { "input_tokens": 12, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 4, "total_tokens": 16 } } ``` ```json Messages { "id": "msg_5084f823", "type": "message", "role": "assistant", "model": "sonnet", "content": [{"type": "text", "text": "Canberra."}], "stop_reason": "end_turn", "usage": { "input_tokens": 12, "output_tokens": 4, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0 } } ``` The `model` field behaves the same way on all three: it **names the model that served** (`claude-sonnet-5` for a `sonnet` request), and the value can be sent again as-is. `mindshub_air` and `mindshub_blaze` return their own alias. See [Models](https://docs.mindshub.ai/inference/models.md#what-the-responses-model-field-contains). All of it meters into the same [usage summary](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code). --- ## MindsHub in Codex Codex is OpenAI's terminal coding agent. It speaks the [Responses API](https://docs.mindshub.ai/inference/responses.md), so it runs against MindsHub as a custom model provider and **can drive any model in the [catalog](https://docs.mindshub.ai/inference/models.md)**, not just OpenAI's. Codex on Claude Sonnet 5.5 or Kimi K3 is a small config change. **Newly supported, and not yet proven over a long session.** The Responses wire format Codex needs is implemented and tested, but we have not driven a full Codex session end to end against production. Expect the setup below to work; please [tell us](mailto:support@mindsdb.com) if something in a longer session does not. One rough edge we already know about is in [Troubleshooting](#troubleshooting). ### Prerequisites - **Codex CLI installed.** See [OpenAI's install docs](https://developers.openai.com/codex/cli). - **A MindsHub API key** from the [console](https://console.mindshub.ai) ([how keys work](https://docs.mindshub.ai/inference/authentication.md)). Create a dedicated key for Codex so it's easy to revoke. ### Check your key first Before touching Codex, confirm the key and the Responses endpoint work: ```bash curl https://api.mindshub.ai/v1/responses \ -H "Authorization: Bearer $MINDSHUB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "gpt", "input": "say OK"}' ``` A `401` means authentication failed; fix that first. A healthy response is a Responses body with `"output_text": "OK"`. ### Configure Codex Add MindsHub as a model provider in `~/.codex/config.toml`: ```toml model = "gpt" model_provider = "mindshub" [model_providers.mindshub] name = "MindsHub" base_url = "https://api.mindshub.ai/v1" wire_api = "responses" auth = { command = "printenv", args = ["MINDSHUB_API_KEY"] } stream_idle_timeout_ms = 1800000 ``` Then export the key and run it: ```bash export MINDSHUB_API_KEY="mdb_..." codex ``` The five provider fields: - **`base_url`** must end in `/v1`. Codex appends `/responses` itself. (This is the opposite of the [Claude Code setup](https://docs.mindshub.ai/inference/kimi-claude-code.md), where the base URL is the bare host; the two agents follow their own vendors' conventions.) - **`auth`** runs the given command and sends its output as the bearer token — `printenv MINDSHUB_API_KEY` reads the key from your environment, so the key still never goes in the config file. Command-backed auth is also what makes Codex ask MindsHub for its model catalog, which is what fills the in-session `/model` picker with your aliases; the simpler `env_key = "MINDSHUB_API_KEY"` setting authenticates identically but leaves the picker on Codex's built-in list. Codex rejects a provider that sets both. (On Windows, point `command` at anything that prints the key, e.g. `powershell -Command "$env:MINDSHUB_API_KEY"`.) - **`wire_api = "responses"`** is required. Codex removed its Chat Completions mode in early 2026. - **`stream_idle_timeout_ms`** is how long Codex waits for the next stream event before it drops the turn and sends it again, up to five times. It counts events only, not the `: keepalive` lines that hold the connection open, and MindsHub sends no event while a model reasons (see [Scope and limits](#scope-and-limits)). So on Codex's default of 300000 (five minutes), a turn that reasons for longer than that without output fails after five re-sends. MindsHub itself ends a stream that sends nothing but keepalives for 30 minutes (see [Streaming](https://docs.mindshub.ai/inference/streaming.md#long-running-requests)), so 1800000 lets a turn reason until that limit, and a larger value adds no time. A turn that reasons longer than that without output still fails. Its stream ends at the limit and Codex sends the turn again, up to five times, each as a new request. If every attempt reasons that long, the turn fails after about three hours. - **`name`** is a display label only. The top-level `model` is the default for new sessions, and it takes a [catalog alias](https://docs.mindshub.ai/inference/models.md) such as `gpt`, `sonnet`, `kimi`, or `deepseek-v4-flash`, not a provider model ID. ### Switch models Per run: ```bash codex --model sonnet # Claude Sonnet 5.5, in Codex codex --model kimi # Kimi K3 codex --model deepseek-v4-flash # DeepSeek V4.1 Flash codex --model gpt-codex # GPT 5.3 Codex ``` Or keep a few profiles in `config.toml` and select one with `--profile`: ```toml [profiles.claude] model = "sonnet" model_provider = "mindshub" [profiles.cheap] model = "deepseek-v4-flash" model_provider = "mindshub" ``` ```bash codex --profile cheap ``` Inside a running session, type `/model` to open Codex's model picker. With the `auth` command config above, the picker lists the MindsHub [catalog](https://docs.mindshub.ai/inference/models.md) — each alias, its label underneath, and the [reasoning effort](https://docs.mindshub.ai/inference/concepts.md#reasoning-effort) levels it supports — instead of Codex's built-in OpenAI list. Any alias from `GET /v1/models` works. Because every model resolves through the same key, switching costs you nothing but the edit. ### Verify it Ask Codex something trivial in a session, then confirm MindsHub metered it. The row should show the alias you ran: ```bash curl "https://auth.mindshub.ai/v1/usage/summary/?range=period&group_by=model" \ -H "Authorization: Bearer $MINDSHUB_API_KEY" ``` If a row appears for your alias, the integration is working end to end: request, generation, and metering. ### Cost and billing - **Codex shows token counts, not costs.** Its session stats carry no MindsHub prices, so the [usage summary](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code) is the only real number. - **Everything lands on one balance.** Codex, [Claude Code](https://docs.mindshub.ai/inference/kimi-claude-code.md), [VS Code](https://docs.mindshub.ai/inference/vscode.md), and your application traffic all draw the same prepaid wallet and appear in the same per-model summary. - **Agents are token-hungry.** Long system prompts and large file contexts add up; see the [price list](https://docs.mindshub.ai/inference/billing.md#price-list) and pick accordingly. `deepseek-v4-flash` and `kimi` are markedly cheaper than `opus` for bulk work. - **Prompt caching helps.** Codex resends context each turn; the prompt cache absorbs most of that at about a tenth of the input price on most models. ### Scope and limits - **Codex keeps its own context.** It sends `store: false` and resends its full input every turn, so none of it is stored server-side and `previous_response_id` never comes into play. Chaining is available to other clients; see [Conversation state](https://docs.mindshub.ai/inference/conversation-state.md). - **Auto-compaction uses a conservative window.** Codex compacts a long session when it approaches the model's context window, and MindsHub publishes a floor rather than a per-model figure, so compaction can fire earlier than a large-window model strictly needs. Raise it for a session with `model_context_window` in `config.toml` if you'd rather keep more history. - **Sampling parameters are adapted per model.** If Codex sends a sampling parameter the target model doesn't take, MindsHub drops it and serves the request rather than failing, and reports it in `X-MindsHub-Dropped-Params` (except on a stream whose `200` went out early; see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers)). See [Core concepts](https://docs.mindshub.ai/inference/concepts.md#parameters-are-adapted-not-rejected). - **Reasoning round-trips, but isn't displayed live.** With `include: ["reasoning.encrypted_content"]` — which Codex sends — a `reasoning` item comes back carrying an opaque blob, so a reasoning model's tool loop stays valid across turns. Any summary arrives with that item at the end of the turn rather than streaming as it is produced, so Codex shows no live "thinking" text. Raw chain-of-thought is never exposed. Reasoning tokens bill as output. ### Troubleshooting | Symptom | Cause and fix | |---|---| | `404` or `Not Found` | `base_url` must end in `/v1`. | | `401` | The key is wrong or revoked, or `MINDSHUB_API_KEY` isn't exported in the shell running `codex`. The `auth` command only reads the variable; it never stores the key. | | `/model` shows only OpenAI models | The provider is using `env_key` instead of the `auth` command block, so Codex never asks MindsHub for its catalog. Switch to the config above and restart Codex. | | `model_not_found` | `model` isn't a catalog alias. Check [`GET /v1/models`](https://docs.mindshub.ai/inference/models.md): it's `kimi`, not `kimi-k3`. | | Requests fail only when streaming | Confirm `wire_api = "responses"`. A provider left on a chat wire will not work. | | `Reconnecting... 1/5`, then `stream disconnected before completion: idle timeout waiting for SSE` | The model reasoned for longer than Codex's `stream_idle_timeout_ms` (five minutes by default) without writing output. Raise it to 1800000 in the provider block; see [Configure Codex](#configure-codex). A larger value adds no time, because MindsHub ends a stream that sends nothing but keepalives for 30 minutes. Past that, lower the reasoning effort or split the task. | | Replies truncate mid-answer | The model's internal reasoning used the output budget. Choose a model with adjustable [reasoning effort](https://docs.mindshub.ai/inference/concepts.md#reasoning-effort), or raise the output cap. | | `429` | Throughput limit, not funding. Back off; see [Rate limits](https://docs.mindshub.ai/inference/rate-limits.md). | | "We're currently experiencing high demand" with no other detail | Codex maps most non-2xx responses to one generic retryable error and retries the turn silently before reporting this, so the real cause is hidden. It is usually a rate limit or an exhausted balance: check [usage and balance](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code), and try the same request with `curl` to see the actual error body. | ### See also - [Coding agents](https://docs.mindshub.ai/inference/coding-agents.md): Claude Code and Codex side by side, with model-choice guidance. - [Responses API](https://docs.mindshub.ai/inference/responses.md): the format Codex speaks, in full. --- ## Coding agents Terminal coding agents are hard-wired to their vendor's API: Claude Code speaks [Anthropic Messages](https://docs.mindshub.ai/inference/messages.md), Codex speaks [OpenAI Responses](https://docs.mindshub.ai/inference/responses.md). MindsHub speaks both, so one key covers both agents, and **either agent can drive any model in the [catalog](https://docs.mindshub.ai/inference/models.md)**. You get three things a vendor-native setup doesn't: - **Model switching.** Run Claude Code on Kimi K3 or DeepSeek V4.1 Flash while its own tools (file edits, shell, search) keep working. - **Centralized billing.** Both agents, plus your application traffic, draw one prepaid balance and appear in one [usage summary](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code). No per-vendor subscriptions to reconcile. - **One credential to rotate.** A single MindsHub key, revocable from the console, instead of an Anthropic key plus an OpenAI key plus whatever else. ### Claude Code Two environment variables: ```bash export ANTHROPIC_BASE_URL="https://api.mindshub.ai" # host only, no /v1 export ANTHROPIC_AUTH_TOKEN="$MINDSHUB_API_KEY" # AUTH_TOKEN, not API_KEY claude --model kimi ``` - **`ANTHROPIC_AUTH_TOKEN`, not `ANTHROPIC_API_KEY`.** The auth-token variable sends `Authorization: Bearer …`, the only header MindsHub accepts. `ANTHROPIC_API_KEY` sends `x-api-key` and gets a 401 before the request reaches the API. If both are set, the auth token wins. - **The base URL is the host only.** The client appends `/v1/messages` itself; adding `/v1` yields a confusing model error rather than a URL error. - **Optionally isolate the profile.** `export CLAUDE_CONFIG_DIR="$HOME/.claude-mindshub"` keeps MindsHub sessions separate from your claude.ai login. Not required for auth, since the auth token outranks a stored login, but it's the fix if a startup 401 shows Claude Code preferring the stored login anyway. Switch models mid-session with `/model`, or start on one: ```bash claude --model sonnet # Claude Sonnet 5.5 claude --model kimi # Kimi K3 claude --model deepseek-v4-flash # DeepSeek V4.1 Flash claude --model gpt # GPT 5.6 Sol, in Claude Code ``` Claude Code's own model picker also works unmodified: real Claude model names like `claude-sonnet-5` or `claude-opus-5[1m]` map onto the matching alias by family. Claude Code can even run its main loop and its subagents on different catalog models; see [Mix models in one session](https://docs.mindshub.ai/inference/kimi-claude-code.md#mix-models-in-one-session). Deep guide, with verified troubleshooting: [Kimi K3 in Claude Code](https://docs.mindshub.ai/inference/kimi-claude-code.md). ### OpenAI Codex Codex speaks the [Responses format](https://docs.mindshub.ai/inference/responses.md), on any catalog model. The full guide is the [Codex guide](https://docs.mindshub.ai/inference/codex.md). Add MindsHub as a model provider in `~/.codex/config.toml`: ```toml model = "gpt" model_provider = "mindshub" [model_providers.mindshub] name = "MindsHub" base_url = "https://api.mindshub.ai/v1" wire_api = "responses" auth = { command = "printenv", args = ["MINDSHUB_API_KEY"] } stream_idle_timeout_ms = 1800000 ``` Then export the key and run it: ```bash export MINDSHUB_API_KEY="mdb_..." codex ``` - **`base_url` includes `/v1`** here, the opposite of the Claude Code setup: Codex follows the OpenAI convention. - **`auth` runs a command that prints the key** — here, reading it from your environment, so the key never goes in the config file. Command-backed auth is also what makes Codex fetch the MindsHub catalog for its in-session `/model` picker; see the [Codex guide](https://docs.mindshub.ai/inference/codex.md#configure-codex) for why. - **`wire_api = "responses"`** is required. Codex removed its Chat Completions mode in early 2026. - **`stream_idle_timeout_ms`** raises how long Codex waits for the next stream event from its five-minute default. Codex does not count keepalives, and MindsHub sends no event while a model reasons, so on the default a turn that reasons for more than five minutes fails. 1800000 matches MindsHub's own limit: it ends a stream that sends nothing but keepalives for 30 minutes. See the [Codex guide](https://docs.mindshub.ai/inference/codex.md#configure-codex). Switch models by changing `model` in the config, or per run: ```bash codex --model sonnet # Claude Sonnet 5.5, in Codex codex --model kimi # Kimi K3 codex --model gpt-codex # GPT 5.3 Codex ``` Deep guide: [MindsHub in Codex](https://docs.mindshub.ai/inference/codex.md). ### VS Code and other editors VS Code chat reaches MindsHub through Copilot's Custom Endpoint provider, which speaks Chat Completions. Chat and agent mode both work, including tool calling. See [MindsHub in VS Code](https://docs.mindshub.ai/inference/vscode.md). Other tools that speak Chat Completions to a custom base URL generally work the same way: point them at `https://api.mindshub.ai/v1` with a catalog alias as the model name, and test before relying on features beyond chat (the [accepted-and-ignored list](https://docs.mindshub.ai/inference/chat-completions.md#request-parameters) applies). ### Picking a model for agent work Agents consume tokens quickly: long system prompts, large file contexts, many turns. That makes model choice a cost decision as much as a quality one. | Alias | Model | Why you'd pick it | |---|---|---| | `sonnet` | Claude Sonnet 5.5 | Strong general coding, mid-range price | | `opus` | Claude Opus 5.5 | Hardest reasoning tasks, most expensive | | `gpt-codex` | GPT 5.3 Codex | Tuned for code | | `kimi` | Kimi K3 | Agentic coding at lower cost | | `deepseek-v4-flash` | DeepSeek V4.1 Flash | Cheapest capable option for bulk work | Prices are in the [price list](https://docs.mindshub.ai/inference/billing.md#price-list). Two cost notes specific to agents: - **Claude Code's default model maps to `opus`**, one of the priciest in the catalog. Pick a cheaper model explicitly if cost matters. - **Caching works in your favor.** Agents cache aggressively. On `mindshub_air`, a cached-read token consumes one allowance unit while uncached input consumes ten and output consumes sixty. Cache writes consume twelve units and bill only after the allowance is exhausted. ### The cost display in your agent is not your bill Claude Code prices sessions locally at its vendor's list rates, so on a non-Claude model its dollar figure is wrong. Codex reports token counts with no dollar figure at all. Neither knows MindsHub's prices for the model actually serving the request. The authoritative number is always: ```bash curl "https://auth.mindshub.ai/v1/usage/summary/?range=period&group_by=model" \ -H "Authorization: Bearer $MINDSHUB_API_KEY" ``` That returns per-model tokens and cost for the current billing period, across every agent and application on the key. ### Separate keys are for revocation Give each machine and agent its own key from the [console](https://console.mindshub.ai): revoking your laptop's key then doesn't touch production. Don't expect separate keys to split your bill, though. Usage is reported for the organization as a whole, grouped by model only, so two keys on the same account meter into the same rows. ### Troubleshooting | Symptom | Cause and fix | |---|---| | `401` at startup in Claude Code | Key is in `ANTHROPIC_API_KEY`, which sends the wrong header: use `ANTHROPIC_AUTH_TOKEN`. Or a stored claude.ai login is winning: set `CLAUDE_CONFIG_DIR` to a fresh directory. | | `There's an issue with the selected model` | Either the model isn't a catalog alias (`kimi`, not `kimi-k3`), or `ANTHROPIC_BASE_URL` has a `/v1` suffix it shouldn't. | | Codex `404` / `Not Found` | `base_url` must end in `/v1` for Codex. | | Codex ignores the key | Confirm `MINDSHUB_API_KEY` is exported in the shell running `codex` — the `auth` command reads the variable at startup. | | Replies truncate mid-answer | The model's internal reasoning used the output budget. In Claude Code, set `CLAUDE_CODE_MAX_OUTPUT_TOKENS=8192` or higher and restart. | | `429` under heavy agent use | Throughput limit, not funding. See [Rate limits](https://docs.mindshub.ai/inference/rate-limits.md). | More symptoms in [Errors](https://docs.mindshub.ai/inference/errors.md) and in each agent's deep guide. --- ## Core concepts A handful of ideas explain most of how MindsHub behaves. ### Aliases You address models by a short, stable name (`sonnet`, `gpt`, `kimi`), not by a provider version string. The alias resolves server-side to a concrete model at the provider. ```json {"model": "sonnet"} ``` Aliases exist so that a provider version bump doesn't become a deploy on your side. When Claude Sonnet 5.5 is superseded, `sonnet` follows it and your code doesn't change. Two consequences: - **Provider model IDs also resolve**, so `claude-sonnet-5` works as `model`. [Messages](https://docs.mindshub.ai/inference/messages.md#model-names) additionally maps Claude family names onto aliases so Claude Code's picker works unmodified. - **Responses name the model that served**, except on `mindshub_air` and `mindshub_blaze`, which return their own alias. [Decisions](https://docs.mindshub.ai/inference/decisions.md#models-and-versions) returns the served Jev version. Log both your requested alias and the returned `model`. The live list is `GET /v1/models`; the catalog is in [Models](https://docs.mindshub.ai/inference/models.md). ### Passthrough MindsHub does not host models. Each request is translated into the target provider's format, sent to that provider, and translated back into the format you asked in. There is no MindsHub model sitting in the middle rewriting your prompts. This is why capability tracks the underlying model: if a model can't see images, MindsHub can't change that. The catalog identifies the configured provider. Jev runs at TypeSafe and preserves its native question-and-answer format through the Decisions endpoint. ### Three APIs, one engine [Chat Completions, Responses, and Messages](https://docs.mindshub.ai/inference/choosing-an-api.md) are three wire formats over the same inference pipeline. Any chat model is reachable from all three. Pick the one your existing code speaks. The formats differ in shape, not in power. Where a genuine capability gap exists (explicit cache breakpoints, the token-counting endpoint), it's flagged in the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md). [Decision models](https://docs.mindshub.ai/inference/decision-models.md) use their own `/v1/decisions` endpoint. They return categories, probabilities, and scores from shared state. Embedding models use `/v1/embeddings`. ### Parameters are adapted, not rejected The following rules apply to the three chat APIs. [Decisions](https://docs.mindshub.ai/inference/decisions.md#request-parameters) preserves its native fields and uses provider validation. Models disagree about which generation parameters they accept. MindsHub is a router, so instead of failing a request over a parameter the target model doesn't take: - A parameter the model supports is passed through at whatever value you sent. - A parameter it doesn't support is **dropped**, and named in `X-MindsHub-Dropped-Params`. - A value above the model's range (`max_tokens` over its ceiling, a `reasoning_effort` above its ladder) is **clamped** down, and named in `X-MindsHub-Clamped-Params` as `name=requested>applied`. - An unknown top-level field is ignored silently. Neither header appears when nothing changed, or on a stream whose `200` went out early, which carries no `X-MindsHub-*` header whatever changed (see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers)). SDKs and coding agents treat a 400 as a hard failure, which is why unsupported parameters are dropped rather than bounced. The boundaries: - A model can restrict the *values* of a parameter it supports, and that error passes through as the provider's 400: Kimi K3 takes `temperature` only at `1` and `top_p` only at `0.95`. - A model can refuse two parameters it supports when they arrive together. Claude Haiku 4.5 (`haiku`, `haiku-4-5`) takes `temperature` or `top_p`, not both: when both are sent, `top_p` is dropped and named in `X-MindsHub-Dropped-Params`. The [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md) notes each such pair. - Unknown *nested* content, such as an unrecognized block inside `messages`, can still reach the provider and fail there. - Structured output is the one exception to the drop rule — see below. ### Structured output Ask for a JSON schema and you get JSON matching it, on all three APIs: `response_format` on Chat Completions, `text.format` on Responses, `output_config.format` on Messages. **This is the one parameter that is refused rather than dropped.** A model that cannot constrain its output returns `400 param_not_supported`, because prose handed to a caller who is about to `json.loads()` it fails later and less legibly than an error here. Samples, the schema-less mode, and the per-model rules are in [Structured output](https://docs.mindshub.ai/inference/structured-output.md). ### Reasoning effort Many current models reason before answering. Where it's adjustable, it's a parameter (`reasoning_effort`, `reasoning.effort`, `output_config.effort`) whose levels vary by model; `GET /v1/models` is authoritative. Where it isn't adjustable, it still happens: `mindshub_air` and `kimi` reason internally on every request. Either way, reasoning bills as **output tokens**, so give reasoning models a few hundred tokens of `max_tokens` headroom. Levels, clamping, and extended thinking are in [Reasoning](https://docs.mindshub.ai/inference/reasoning.md). ### Funding: included allowance and wallet Two separate pots pay for requests. **Included allowance**: a recurring weighted allowance usable on `mindshub_air`. Cached reads consume less than uncached input, cache writes, or output. Read `included_percent_remaining` and `next_refresh_at` for the current window. **The wallet**: a prepaid, organization-level balance that funds paid model requests, embeddings, and web search. On `mindshub_air` a cache write draws the allowance like any other prompt token, and a web search draws it too, and each only bills past it. Jev is priced at $0 during its launch promotion and never draws the included allowance. An organization with wallet credit, or with a card that has completed a top-up and has no payment error, calls it without a daily cap. Every other organization gets a free daily allowance while shared free capacity lasts, and past it gets `402 wallet_empty`. Permissions and rate limits apply to every organization. See [Billing](https://docs.mindshub.ai/inference/billing.md#jev-decisions). For paid requests and Jev decisions, funding is checked *before* the model runs, so an unfunded request is refused up front and costs nothing. Running out is not the same as going too fast: | | Meaning | Fix | |---|---|---| | `429 rate_limited` | Too fast | Back off, honor `Retry-After` | | `429 included_allowance_exhausted` | Included allowance exhausted | Wait for `reset_at` or add credit. No `reset_at` means your organization's included allowance is zero, so add credit | | `402 wallet_empty` | Wallet empty, or Jev's free daily allowance used up | Top up. On Jev, the allowance also refills by itself over 24 hours | Only the first is fixed by retrying. Details in [Billing](https://docs.mindshub.ai/inference/billing.md) and [Rate limits](https://docs.mindshub.ai/inference/rate-limits.md). ### Conversations live client-side, except on Responses On Chat Completions and Messages every request carries its full history. [Responses](https://docs.mindshub.ai/inference/conversation-state.md) is the exception: turns are stored by default and chain by `previous_response_id`, so a client can send only its new input. Resending history is cheaper than it looks because [prompt caching](https://docs.mindshub.ai/inference/prompt-caching.md) is automatic across the catalog. ### Terms in one line each | Term | Meaning | |---|---| | **Alias** | Short stable model name you put in `model` | | **Passthrough** | Requests are translated and proxied to the upstream provider | | **Included allowance** | Recurring weighted allowance on `mindshub_air` | | **Wallet** | Prepaid balance funding everything else | | **Reasoning effort** | How hard a model thinks before answering; billed as output | | **Dropped / clamped** | Parameters adapted to the target model, reported in headers | | **Cache write** | Prompt tokens stored for reuse; draws the `mindshub_air` allowance at weight 12 and bills past it | --- ## Conversation state On Chat Completions and Messages every request carries its full history: keep the message list client-side, append to it each turn, and send it. Responses is the exception: turns are stored by default and chain by `previous_response_id`, so a client can send only its new input. ### Works with Every model. Server-side state is a property of the Responses API, not of any model: `store` and `previous_response_id` work on `sonnet`, `kimi`, and `gpt` alike. ### Request ```python Chat Completions messages = [{"role": "user", "content": "My name is Quill."}] first = client.chat.completions.create(model="mindshub_air", messages=messages) messages.append(first.choices[0].message) messages.append({"role": "user", "content": "What's my name?"}) second = client.chat.completions.create(model="mindshub_air", messages=messages) print(second.choices[0].message.content) ``` ```python Responses first = client.responses.create(model="mindshub_air", input="My name is Quill.") second = client.responses.create( model="mindshub_air", input="What's my name?", previous_response_id=first.id, ) print(second.output_text) ``` ```python Messages messages = [{"role": "user", "content": "My name is Quill."}] first = client.messages.create(model="mindshub_air", max_tokens=256, messages=messages) messages.append({"role": "assistant", "content": first.content}) messages.append({"role": "user", "content": "What's my name?"}) second = client.messages.create(model="mindshub_air", max_tokens=256, messages=messages) print(second.content[0].text) ``` ```typescript Chat Completions const messages: any[] = [{ role: "user", content: "My name is Quill." }]; const first = await client.chat.completions.create({ model: "mindshub_air", messages }); messages.push(first.choices[0].message, { role: "user", content: "What's my name?" }); const second = await client.chat.completions.create({ model: "mindshub_air", messages }); console.log(second.choices[0].message.content); ``` ```typescript Responses const first = await client.responses.create({ model: "mindshub_air", input: "My name is Quill." }); const second = await client.responses.create({ model: "mindshub_air", input: "What's my name?", previous_response_id: first.id, }); console.log(second.output_text); ``` ```typescript Messages const messages: any[] = [{ role: "user", content: "My name is Quill." }]; const first = await client.messages.create({ model: "mindshub_air", max_tokens: 256, messages }); messages.push({ role: "assistant", content: first.content }, { role: "user", content: "What's my name?" }); const second = await client.messages.create({ model: "mindshub_air", max_tokens: 256, messages }); console.log(second.content[0]); ``` Resending the full conversation in `input` every turn also remains valid on Responses, and is what the OpenAI SDK does when you aren't chaining. [Prompt caching](https://docs.mindshub.ai/inference/prompt-caching.md) keeps that cheap: repeated prefixes bill at roughly a tenth of the input rate. ### Server-side state on Responses Responses are stored by default (`store: true`, as on OpenAI). Send `store: false` to keep a turn out of storage entirely; its id is then not retrievable and cannot be chained from. 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. | | `client.responses.cancel(id)` | Accepted only for `background: true` requests; every request completes synchronously, so there is nothing to cancel and the call returns `400` otherwise. | Boundaries worth knowing: - **Stored turns expire after 30 days.** After that the id is a `404` and 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`. A stream whose `200` went out early is the exception: it carries no `X-MindsHub-*` header, so it cannot report the cut (see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers)). - **An unknown `previous_response_id` is a `404`** (`previous_response_not_found`), not a silently context-free answer. - **`instructions` are per turn** and 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 Conversations API) is rejected with a `400`**, as are `prompt` and `context_management`. A client that believes we are holding its thread would otherwise stop sending history and lose context on every turn. ### Response On Responses the `id` on every response is what you chain from. Nothing else about the response shape changes on any API. ### Per-model differences None. State is handled before the request reaches a provider. ### Errors you can hit `404 previous_response_not_found` for an unknown, expired, or deleted id; `400 unsupported_parameter` for `conversation`, `prompt`, or `context_management`; `400` from `cancel` on a non-background response. See [Errors](https://docs.mindshub.ai/inference/errors.md). --- ## Decision models A decision model evaluates information and returns values your application can act on: a category, a position on a scale, or the probability that a statement is true. **Jev** is TypeSafe's decision model, available through the MindsHub Inference API. TypeSafe calls this class of model **System One**: models designed for focused judgments that fit inside a larger software workflow. For example, a delivery report might need three judgments: which team should handle it, whether the customer requested a replacement, and how much damage is described. Jev can answer all three in one request. Your application then routes the report and decides whether to ask for more information. **Start with the [runnable example](https://docs.mindshub.ai/inference/decisions.md#quick-example)** if you prefer to learn by doing. It sends all three question types with your existing MindsHub key. No TypeSafe account or SDK is required. ### From information to a decision Every request has three ingredients: | Ingredient | What you provide | Delivery-report example | |---|---|---| | `state` | The information to inspect | The customer's report and relevant delivery details | | `questions` | Named judgments, each with a type and instructions or criteria | `team`, `replacement`, `damage` | | `model` | A MindsHub decision-model alias | `jev` | The response puts each result under the same name in `answers`. Reading `answers.team.choice` gives the selected team directly. You do not extract JSON from a generated chat message. The sequence is simple: **collect the relevant facts → ask Jev → inspect the answer and uncertainty → run your application logic**. The endpoint returns judgments; it does not call your tools, change records, or execute the action named by an answer. ### When to use a decision model | Your application needs… | Use… | Example | |---|---|---| | A semantic judgment with a defined answer shape | [Decisions](https://docs.mindshub.ai/inference/decisions.md) | Pick a queue, assess relevance, flag whether a message asks for follow-up | | Generated content or a flexible JSON structure | A chat model, optionally with [structured output](https://docs.mindshub.ai/inference/structured-output.md) | Draft a reply, summarise a document, extract a variable-length list | | An exact calculation or a known rule | Ordinary code | Compare dates, add invoice amounts, enforce permissions | Structured output constrains a chat model's generated response to your schema. Decisions gives you three specific question types with probabilities as part of their answer contract. Choose based on the result your code needs. You can use both in a workflow: let Jev identify the relevant queue, then use a chat model to draft a response using that queue's instructions. ### Pick the question type #### Noul: how likely is “yes”? Use `"type": "noul"` for one yes/no proposition. The name is part of the API; the returned `noul` value is a number between 0 and 1. ```json { "type": "noul", "instructions": "Does the report explicitly ask for a replacement item?" } ``` A value near 1 supports “yes”; near 0 supports “no”. Around 0.5, neither answer has a clear lead. **0.5 does not mean half a replacement, medium damage, or a Boolean false.** There is no separate `confidence` field for this type. Keep the number until your code applies a threshold. For example, you might treat values at least 0.9 as a request for replacement, at most 0.1 as no request, and the rest as needing clarification. These are example thresholds to evaluate on your own reports, not service guarantees. If the boundary needs explanation, supply `criteria.true` and `criteria.false`; see the [request reference](https://docs.mindshub.ai/inference/decisions.md#request-parameters). #### Choice: which category fits? Use `"type": "choice"` when the result must be one of a set of named options. `criteria` maps the names your code uses to descriptions of when they apply. ```json { "type": "choice", "instructions": "Which team should review this delivery report?", "criteria": { "packaging": "Damage to the packaging, with the item itself intact.", "product": "Damage to the item itself.", "other": "A different issue, or not enough information to identify one." } } ``` Read the selected name from `choice`, each option's probability from `probabilities`, and the distribution's certainty from `confidence`. Keep option names stable if you persist them or use them as routing keys. Include an explicit fallback when real inputs may fit none of your business categories. A model can still select a category on an ambiguous report; your confidence policy determines whether to use it. Choice selects **one** option. For several independent labels that may all apply, ask several noul questions in the same request. #### Score: where does it sit on a scale? Use `"type": "score"` for one ordered dimension. Each entry in `criteria` describes a level, and its array position assigns the number: 0, 1, 2, and so on. ```json { "type": "score", "instructions": "How much damage does the report describe?", "criteria": [ "Both packaging and item are undamaged.", "Packaging is damaged; the item is intact and usable.", "The item is damaged and cannot be used normally." ] } ``` Here the returned `score` lies between 0 and 2. It is the probability-weighted average of the level numbers, so it can be fractional. For example, probabilities `{"0": 0.1, "1": 0.6, "2": 0.3}` produce `0 × 0.1 + 1 × 0.6 + 2 × 0.3 = 1.2`. Read `legend` to map the numbers back to your descriptions. Both `legend` and `probabilities` use **string keys** in JSON, such as `"1"`. Keep the score as a number when sorting reports. If your code requires one category, prefer a choice question or define a conversion rule explicitly. A score is not a measured damage percentage, a dollar amount, or an array index you can use without conversion. ### Probability and confidence For choice and score, `probabilities` describes the distribution across the options or levels. `confidence` is a separate value from 0 to 1 derived by TypeSafe from that distribution. **It is not necessarily the selected option's probability, and it is not a guarantee of correctness.** Use the returned value rather than substituting the largest probability. See TypeSafe's [confidence explanation](https://docs.typesafe.ai/confidence). Record both when evaluating an integration. Two answers with a similar score can distribute probability differently; a routing policy that looks only at the score loses that information. Noul has no separate confidence value: use the probability of the proposition itself. Here is a routing policy for the [endpoint example](https://docs.mindshub.ai/inference/decisions.md#quick-example). It chooses a destination but performs no external action: ```python Python # `result` is the parsed response from the quick example. team = result["answers"]["team"] minimum_confidence = 0.8 # Illustrative; tune against labelled reports. if team["confidence"] < minimum_confidence or team["choice"] == "other": destination = "manual_review" else: destination = { "packaging": "packaging_queue", "product": "product_queue", }[team["choice"]] print(destination) ``` An unclear answer is a useful outcome. Keep a review or clarification path instead of forcing every report into automatic handling. Measure incorrect automatic routes and the proportion sent for review when choosing your threshold. ### Give each question the context it needs `state` can be a string, a JSON object, or an array. An object helps label several pieces of related information: ```json { "report": "The outer box arrived torn. The item inside is undamaged and works normally.", "delivery": {"reference": "B-204", "status": "delivered"}, "previous_contact": "No earlier report for this delivery." } ``` This is **one state**, not a batch of independent evaluations. An array is also one shared state. All questions in a request see that state. Send separate requests for unrelated reports, or identify the exact item each question refers to. Question names such as `damage` are identifiers for your code; TypeSafe does not use those names as model instructions. Put the actual question in `instructions` and the distinctions in `criteria`. Write each description so it can be understood on its own: “the item cannot be used normally” is clearer than “worse than above”. Questions are evaluated independently. A question cannot read another answer in the same request. If a second judgment depends on the first result, make a second call with the relevant result included in its state. If it only depends on the same original facts, put both questions in the first call. Jev currently accepts text and structured JSON, not images, audio, or video. Sending an image URL in a field does not make it a vision request. Extract the text or observations you need first. See the provider's [state guide](https://docs.typesafe.ai/concepts/state). ### Design and evaluate your questions 1. **Define the output your code needs.** Choose a category, a yes/no proposition, or one ordered dimension before writing the request. 2. **Make distinctions explicit.** Include boundary cases in the instructions or criteria. An instruction such as “is this bad?” leaves your application's rule undefined. 3. **Separate judgments.** Ask about damage and replacement intent separately. Your code can combine them later without hiding how each contributed. 4. **Test representative inputs.** Include straightforward cases, missing facts, conflicting statements, out-of-category reports, and the languages you expect to receive. Inspect wrong answers as well as low-confidence ones. 5. **Version the evaluation.** Record your question definitions, application thresholds, requested alias, returned model version, answers, and expected outcomes. Re-run the same cases before changing any of those inputs. Keep exact arithmetic, date comparisons, access checks, and action execution in code. Jev's structured answer does not make an arbitrary calculation exact. User-supplied state can also contain misleading instructions; a model answer should never grant permissions. TypeSafe documents these limitations for [Jev 1.13](https://docs.typesafe.ai/model-jaggedness/jev-1.13). For request fields, examples, billing, limits, and retry behavior, continue to the [Decisions endpoint](https://docs.mindshub.ai/inference/decisions.md). For the provider's terminology and background, see [TypeSafe's System One introduction](https://docs.typesafe.ai/concepts/system-one). --- ## Decisions (Jev) Evaluate information with **Jev**, a decision model hosted by TypeSafe, through your MindsHub API key. Send a `state` and named `questions`; receive matching `answers` with probabilities and token usage. New to this kind of model? Read [Decision models](https://docs.mindshub.ai/inference/decision-models.md) for when to use it and how to interpret its answers. ### Endpoint `POST https://api.mindshub.ai/v1/decisions` Send `Authorization: Bearer $MINDSHUB_API_KEY` and `Content-Type: application/json`. Get your key from the [MindsHub console](https://console.mindshub.ai/) and keep it on your server, outside source control. See [Authentication](https://docs.mindshub.ai/inference/authentication.md). This is a MindsHub HTTP endpoint. You do not need a TypeSafe key or SDK. Chat SDK methods such as `chat.completions.create()` use a different request format; use an ordinary HTTP client for decisions. ### Quick example This example evaluates one delivery report in three ways: selects a team (`choice`), checks for an explicit replacement request (`noul`), and rates damage (`score`). Python uses only the standard library. The TypeScript example uses native `fetch` in Node.js 20+ and runs as an ES module. Set `MINDSHUB_API_KEY` in your server environment, then run the example for your language. The same JSON body works in every HTTP client. ```python Python import json import os from urllib.error import HTTPError from urllib.request import Request, urlopen payload = { "model": "jev", "state": {"report": "The outer box arrived torn. The item inside is undamaged and works normally."}, "questions": { "team": { "type": "choice", "instructions": "Which team should review this delivery report?", "criteria": { "packaging": "Damage to the packaging, with the item itself intact.", "product": "Damage to the item itself.", "other": "A different issue, or not enough information to identify one.", }, }, "replacement": { "type": "noul", "instructions": "Does the report explicitly ask for a replacement item?", }, "damage": { "type": "score", "instructions": "How much damage does the report describe?", "criteria": [ "Both packaging and item are undamaged.", "Packaging is damaged; the item is intact and usable.", "The item is damaged and cannot be used normally.", ], }, }, } request = Request( "https://api.mindshub.ai/v1/decisions", headers={"Authorization": f"Bearer {os.environ['MINDSHUB_API_KEY']}", "Content-Type": "application/json"}, data=json.dumps(payload).encode(), ) try: with urlopen(request, timeout=40) as response: result = json.load(response) except HTTPError as error: raise SystemExit(f"HTTP {error.code}: {error.read().decode()}") from None print(json.dumps(result, indent=2)) print("Team:", result["answers"]["team"]["choice"]) print("Replacement requested (probability):", result["answers"]["replacement"]["noul"]) print("Damage (0–2):", result["answers"]["damage"]["score"]) ``` ```typescript TypeScript const apiKey = process.env.MINDSHUB_API_KEY; if (!apiKey) throw new Error("Set MINDSHUB_API_KEY to your MindsHub API key."); const response = await fetch("https://api.mindshub.ai/v1/decisions", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "jev", state: { report: "The outer box arrived torn. The item inside is undamaged and works normally." }, questions: { team: { type: "choice", instructions: "Which team should review this delivery report?", criteria: { packaging: "Damage to the packaging, with the item itself intact.", product: "Damage to the item itself.", other: "A different issue, or not enough information to identify one.", }, }, replacement: { type: "noul", instructions: "Does the report explicitly ask for a replacement item?", }, damage: { type: "score", instructions: "How much damage does the report describe?", criteria: [ "Both packaging and item are undamaged.", "Packaging is damaged; the item is intact and usable.", "The item is damaged and cannot be used normally.", ], }, }, }), signal: AbortSignal.timeout(40_000), }); if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`); const result = await response.json(); console.log(JSON.stringify(result, null, 2)); console.log("Team:", result.answers.team.choice); console.log("Replacement requested (probability):", result.answers.replacement.noul); console.log("Damage (0–2):", result.answers.damage.score); ``` ```bash cURL curl --fail-with-body --max-time 40 https://api.mindshub.ai/v1/decisions \ -H "Authorization: Bearer $MINDSHUB_API_KEY" \ -H 'Content-Type: application/json' \ --data-raw ' { "model": "jev", "state": { "report": "The outer box arrived torn. The item inside is undamaged and works normally." }, "questions": { "team": { "type": "choice", "instructions": "Which team should review this delivery report?", "criteria": { "packaging": "Damage to the packaging, with the item itself intact.", "product": "Damage to the item itself.", "other": "A different issue, or not enough information to identify one." } }, "replacement": { "type": "noul", "instructions": "Does the report explicitly ask for a replacement item?" }, "damage": { "type": "score", "instructions": "How much damage does the report describe?", "criteria": [ "Both packaging and item are undamaged.", "Packaging is damaged; the item is intact and usable.", "The item is damaged and cannot be used normally." ] } } }' ``` The response has the keys `answers.team`, `answers.replacement`, and `answers.damage`. For this report, expect the packaging team, a low probability of a replacement request, and a damage score close to 1. Exact probabilities and token counts can vary. For a dev deployment, replace the URL with `https://api.dev.mindshub.ai/v1/decisions` and use a key for that environment. The examples above use the production address; a model must be activated in the environment's catalog before it can be called. ### Request parameters | Field | Type | Required | Meaning | |---|---|---|---| | `model` | string | Yes | A MindsHub decision alias; see [Models](#models-and-versions). | | `state` | string, object, or array | Yes | The information every question evaluates. Objects and arrays can contain nested JSON values. A top-level number, Boolean, or `null` is not a valid state. | | `questions` | object | Yes | A nonempty map of question names to question definitions. Each answer is returned under its question's name. | | `questions..type` | string | Yes | Exactly `noul`, `choice`, or `score`; determines the criteria and answer shape. | | `questions..instructions` | string, object, array, or `null` | No | The judgment to make. Use a clear sentence for your first integration. | | `questions..criteria` | Depends on type | Depends on type | See below. | #### Criteria for each type | Type | Criteria contract | Guidance | |---|---|---| | `noul` | Optional object with optional `true` and `false` descriptions, or `null`. Each description may be a string, object, array, or `null`. | Define what yes and no mean when the instruction alone is ambiguous. | | `choice` | Required object mapping option names to descriptions. Each description may be a string, object, array, or `null`; the criteria object itself cannot be `null`. | Provide meaningful descriptions and an `other` option when appropriate. | | `score` | Required nonempty array. Each level is a string, object, or array; a level cannot be `null`. Positions start at 0. | Start with a few clearly distinct levels ordered along one dimension. | For example, a noul can make its boundary explicit: ```json { "type": "noul", "instructions": "Does the customer explicitly request a replacement?", "criteria": { "true": "The customer asks for another item to be sent.", "false": "The customer only describes a problem or asks for information." } } ``` Instructions are optional in the wire schema, but the question's name is not an instruction. Always supply the meaning in `instructions`, `criteria`, or both. Prefer complete descriptions to relying on a name such as `urgent`. The gateway preserves omitted fields, explicit `null` values, and additional JSON fields when forwarding the request. TypeSafe applies its own semantic validation. Unknown fields are **not** a promise of additional capabilities: use the documented fields above. In particular, chat parameters such as `messages`, `temperature`, `max_tokens`, tools, and `stream` are not supported controls for this endpoint. The [generated endpoint reference](/api-reference/create-decisions) contains the complete schemas. For interpretation and question design, see [Decision models](https://docs.mindshub.ai/inference/decision-models.md#pick-the-question-type). ### Response One illustrative response to the complete quick example is shown below. Values are simplified for readability; do not assert exact numeric outputs in your application. ```json { "model": "jev-1.13.0", "answers": { "team": { "type": "choice", "choice": "packaging", "confidence": 1.0, "probabilities": {"packaging": 1.0, "product": 0.0, "other": 0.0} }, "replacement": {"type": "noul", "noul": 0.0}, "damage": { "type": "score", "score": 1.0, "confidence": 1.0, "legend": { "0": "Both packaging and item are undamaged.", "1": "Packaging is damaged; the item is intact and usable.", "2": "The item is damaged and cannot be used normally." }, "probabilities": {"0": 0.0, "1": 1.0, "2": 0.0} } }, "usage": {"input_tokens": 420, "output_tokens": 46} } ``` | Field | How to use it | |---|---| | `model` | The actual model version returned by TypeSafe. Record it when comparing results over time. | | `answers` | Map with the same names and question types as your request. Read by name, not object order. | | Noul: `noul` | Probability of yes, from 0 to 1. It is a number, not a Boolean. | | Choice: `choice` | The selected option name from your criteria. | | Choice/score: `probabilities` | Probabilities across the named choices or numbered levels. Allow small floating-point rounding differences when checking their sum. | | Choice/score: `confidence` | TypeSafe's certainty statistic from 0 to 1. It is not necessarily the highest option probability. | | Score: `score` | Probability-weighted mean of the level numbers, between 0 and the last index. May be fractional. | | Score: `legend` | Map from string indices to your original level descriptions. | | `usage.input_tokens`, `usage.output_tokens` | Provider-reported token counts for the whole request, not one counter per question. Both are present even while the price is zero. | ### Models and versions | Send in `model` | Meaning | |---|---| | `jev` | The Jev version adopted by MindsHub; currently `jev-1.13.0`. | | `jev-latest` | An accepted synonym for `jev`. Moves when MindsHub updates its catalog. | | `jev-1.13.0` | The version pinned by this integration. Use it when evaluating changes against a fixed model. | MindsHub's aliases advance through MindsHub catalog updates; a new TypeSafe release does not automatically change them. Pinning a version controls selection, not how long the provider will keep serving it. Use [`GET /v1/models`](https://docs.mindshub.ai/inference/models-endpoint.md) with the same key to discover catalog models. Jev entries have `kind: "decision"`; an accepted synonym need not have its own row. TypeSafe-only aliases such as `jev-preview` are not automatically MindsHub aliases. Unlike the chat and embedding endpoints, the decisions response's `model` reports the served version rather than echoing the requested alias. For example, requesting `jev` currently returns `jev-1.13.0`. ### Limits and billing - **Launch promotion:** Jev input and output both cost **$0** through MindsHub, and requests do not consume the included allowance. See [Billing](https://docs.mindshub.ai/inference/billing.md#jev-decisions). - **Free daily allowance:** an organization with wallet credit, or with a card that has completed a top-up and has no payment error, calls Jev without a daily cap. Every other organization gets a free allowance of 100,000 tokens and 100 requests while shared free capacity lasts, and it refills continuously by that much over every 24 hours. A new organization starts with a full allowance, so it can use roughly twice that on its first day. Past it, this endpoint returns `402 wallet_empty`, and the `X-MindsHub-Recovery-Url` header carries the console path `/billing`, where you [add credit](https://console.mindshub.ai/billing). Organization permissions and rate limits apply to every organization. - **Usage still exists:** input and output token counts are returned and recorded on every call. Zero price does not mean zero tokens, and those tokens count toward the free daily allowance. Check the [usage summary](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code) to reconcile your calls. - **Context:** the current Jev model allows 64,000 tokens for state plus all questions, and 32,000 tokens for state plus the longest individual question. Both limits must be met; TypeSafe enforces them. See the provider's [model limits](https://docs.typesafe.ai/models). - **Several questions:** include independent questions about the same state in one request. The state is shared, while the questions and their criteria still contribute input tokens. This is not a separate batch-jobs API. - **Request rate:** MindsHub's [rate limits](https://docs.mindshub.ai/inference/rate-limits.md) and provider capacity apply. TypeSafe's direct-account limits are not a throughput entitlement for a MindsHub key. ### Endpoint behavior The call is synchronous: it returns one JSON response after evaluation. There is no SSE streaming, background polling, tool execution, or conversation chaining. Each request must contain all the state its questions need. No image, audio, or video input is supported. MindsHub handles authentication, access, model selection, and usage accounting; TypeSafe serves Jev. Successful provider answers and usage are returned in their native JSON shape. The endpoint validates the response before returning it, so an invalid provider response can produce a 503 (`upstream_status: 502`) instead of partial answers. ### Errors and retries Check the HTTP status before reading `answers`. Errors may contain a MindsHub `error` object, a validation `detail` list, or TypeSafe's original body. The examples retain the error status and body rather than assuming one universal shape. | Status | Typical cause | What to do | |---|---|---| | `400` | Wrong model kind, model not configured, or provider rejection | Inspect the code/message. Use a decision model and correct the request; report a configuration failure. | | `401` / `403` | Invalid MindsHub key or insufficient access | Check the key, its environment, and organization permissions. | | `402` | `wallet_empty`: neither wallet credit nor a topped-up card free of payment errors, and the free daily allowance or shared free capacity is used up | Add credit; the `X-MindsHub-Recovery-Url` header carries the console path. Without credit the allowance refills gradually over 24 hours, so an immediate retry is refused again. | | `404` | Alias absent from this environment's catalog or unavailable to this account | Check `GET /v1/models` on the same host with the same key. | | `422` | Missing field, invalid question type/shape, or provider validation | Inspect the field error, fix the payload, and resend. | | `429` / `529` | Rate limit or provider overload | Honor `Retry-After` when supplied; use bounded backoff. | | `503` | Access policy temporarily unavailable | Retry with bounded backoff. | | `503` with `upstream_status: 502` | Provider connection, credential, or response problem | Preserve the response for diagnosis. Check the retry caveat below before resending. | | `503` with `upstream_status: 504` | Provider timeout | The evaluation may already have run; do not assume that resending is free of duplicate work. | The gateway never sends a 502 or 504: the network edge would replace them with its own page and discard the body, so both arrive as a 503 with the original status in `error.upstream_status` (at the top level when the body has no `error` object). Provider validation, rate-limit, and overload errors keep their status and body, including `Retry-After` when supplied. A provider credential failure becomes a 503 with `upstream_status: 502`; it does not mean your MindsHub key needs replacing. Other provider failures may retain their original status. See [Errors](https://docs.mindshub.ai/inference/errors.md) for common MindsHub access failures. The gateway may retry one explicit provider `429` or `529` refusal when the requested delay is at most one second. It does **not** automatically retry ambiguous timeouts or disconnects. There is no documented idempotency key for this endpoint: a repeated POST can execute another evaluation. Even at a $0 price, a repeated evaluation uses rate-limit capacity and, for an organization without credit, its free daily allowance. Every request that passes the funding check counts toward the daily requests, including one the provider refuses or times out, so account for duplicate work before adding client retries. For support, retain the HTTP status, response body, any request ID in the response headers, requested model, and timestamp. Keep API keys out of logs and support messages. --- ## Embeddings(2) `POST /v1/embeddings` follows the OpenAI embeddings shape. Embedding models are a separate part of the catalog: they carry `"embedding": true` in [`GET /v1/models`](https://docs.mindshub.ai/inference/models-endpoint.md) and can't chat, and chat models can't embed. ### Endpoint `POST https://api.mindshub.ai/v1/embeddings` ### Quick example ```python Python import os from openai import OpenAI client = OpenAI( base_url="https://api.mindshub.ai/v1", api_key=os.environ["MINDSHUB_API_KEY"], ) response = client.embeddings.create(model="embed-small", input=["first text", "second text"]) vectors = [item.embedding for item in response.data] ``` ```typescript TypeScript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.mindshub.ai/v1", apiKey: process.env.MINDSHUB_API_KEY, }); const response = await client.embeddings.create({ model: "embed-small", input: ["first text", "second text"] }); const vectors = response.data.map((item) => item.embedding); ``` ### Request parameters | Parameter | Type | Required | Notes | |---|---|---|---| | `model` | string | yes | An embedding alias, currently `embed-small` (1,536 dimensions). | | `input` | string, array of strings, or token array(s) | yes | All OpenAI input forms are accepted. | | `encoding_format` | string | no | `float` (default) or `base64`. | | `dimensions` | integer | no | Forwarded to the model where supported. | | `user` | string | no | Forwarded. | ### Request body The full request and response schemas are on the generated reference page: [POST /v1/embeddings](/api-reference/create-embedding). ### Response ```json { "object": "list", "data": [ {"object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, 0.0789]} ], "model": "text-embedding-3-small", "usage": {"prompt_tokens": 4, "total_tokens": 4} } ``` The real `embedding` array has 1,536 values; it's truncated here. `model` names the model that served, as on the chat endpoints. Embedding calls are input-only: `usage` has `prompt_tokens` and `total_tokens`, no completion tokens. ### Lane-specific behaviour - Embeddings always bill to the wallet and never draw the [included allowance](https://docs.mindshub.ai/inference/billing.md): an empty wallet refuses them like any other paid request. The rate is low ($0.02 per million tokens), but it isn't free. - Sending `embed-small` to `/v1/chat/completions` fails upstream. ### Errors Errors use the OpenAI envelope and follow the standard taxonomy in [Errors](https://docs.mindshub.ai/inference/errors.md); upstream provider errors are relayed with their original status codes. --- ## Errors Every error the API returns, and what to do about each. Debugging a live failure? Skip to the [error catalog](#error-catalog); the body shapes below explain why the JSON you got may not match what your SDK expects. For Jev, use the [Decisions error and retry guidance](https://docs.mindshub.ai/inference/decisions.md#errors-and-retries). Its provider errors retain their native bodies, and an ambiguous timeout is not retried automatically. ### Error shapes Every operation's error responses, with these shapes and the headers each carries, are also listed on its [reference page](/api-reference/create-chat-completion). The shape depends on where the error happens, not just what it is. There are four: **1. The standard error object**: most errors on the OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`, `/v1/models`): ```json { "error": { "message": "The model 'not-a-model' does not exist or you do not have access to it.", "type": "invalid_request_error", "param": "model", "code": "model_not_found" } } ``` A denial with a named cause adds `deny_detail` inside `error`, beside `code`. Today the only value is `model_restricted`, on a `403 permission_denied` for a model an administrator in your organization restricted: ```json { "error": { "message": "An administrator in your organization has restricted the model 'opus'. Choose another model.", "type": "permission_error", "param": "model", "code": "permission_denied", "deny_detail": "model_restricted" } } ``` **2. The Anthropic envelope**: everything on `/v1/messages` and `/v1/messages/count_tokens`: ```json {"type": "error", "error": {"type": "not_found_error", "message": "..."}} ``` The same `deny_detail` appears inside `error` in this envelope too. **3. Plain `{"detail": ...}`**: request-body validation failures (`422` with a list of field errors, plus some `400`s such as an invalid image data URL) and internal server errors (`500`) on most OpenAI-compatible endpoints. An OpenAI SDK will not find an `error.message` in these; read `detail`. (Exception: `/v1/embeddings` internal errors use `{"error": {"type": "internal_error", ...}}`, and its upstream provider errors are relayed with their original OpenAI-shaped bodies.) **4. Gate refusals**: your credentials are checked in front of the API, before the endpoint is chosen, so a `401` carries the Anthropic envelope above on every endpoint, including the OpenAI-compatible ones: ```json {"type": "error", "error": {"type": "authentication_error", "message": "Invalid or missing credentials. See https://docs.mindshub.ai/inference/authentication"}} ``` A gate `403` arrives in the same shape with `error.type` of `permission_error`, and means the credential is real but is not allowed to use that resource. Dispatch on the status code; the message is written for a person and may change. See [Authentication](https://docs.mindshub.ai/inference/authentication.md). Credentials are checked twice: once at the edge, and once at the deeper authorization gate the request passes on its way to a model. Both refusals look the same to you, and both are terminal. **Dispatch on the HTTP status code, and treat the body as best-effort.** The `code` field, where present, is stable and safe to switch on. ### Error catalog | Status | `code` | What happened | What to do | |---|---|---|---| | 401 | none or `invalid_credentials` | Missing, malformed, or revoked credentials: an API key or a browser session. The `code` is absent when the edge refused the credential and `invalid_credentials` when the authorization gate did; both mean the same thing to you. | Fix the credentials. Don't retry. | | 403 | none or `permission_denied` | The credential is real but is not allowed to use this resource, for example a removed organization member or a role without model execution permission. | Fix the credential's access. Don't retry. If you believe the credential should have access, report it to [support@mindsdb.com](mailto:support@mindsdb.com) with the response headers. | | 403 | `permission_denied` with `deny_detail: model_restricted` | An administrator in your organization restricted this model. The body's `error.deny_detail` and the `X-MindsHub-Deny-Detail` header both read `model_restricted`, and the message names the model. [`GET /v1/models`](https://docs.mindshub.ai/inference/models-endpoint.md) lists it with `enabled: false` and `disabled_reason: model_restricted`. | Choose another model. Don't retry, and don't add credit: credit does not lift an administrator's rule. | | 402 | `wallet_empty` | The request needs wallet credit and your organization's balance is empty. This also applies to free-bucket overage when the account should recover through its wallet, and, on `/v1/decisions`, to an organization with neither credit nor a topped-up card free of payment errors, once its free daily Jev allowance or the shared free capacity is used up. | Add credit in the [console](https://console.mindshub.ai); the `X-MindsHub-Recovery-Url` header carries the console path. On a priced model, don't retry until funded. On `/v1/decisions` the call also works again without credit once the free allowance refills, gradually over 24 hours. See [Billing](https://docs.mindshub.ai/inference/billing.md). | | 400 | `max_tokens_exceeded` | `max_tokens` above the hard cap of 131,072. | Lower `max_tokens`. | | 400 | `model_not_configured` | The model is in the catalog but isn't currently routable. Rare. | Use another model, and report it to [support@mindsdb.com](mailto:support@mindsdb.com). | | 404 | `model_not_found` | Unknown model name: a typo, a raw provider ID, or an alias not in the catalog. | Use an alias from [`GET /v1/models`](https://docs.mindshub.ai/inference/models.md#alias-rules). | | 422 | none (`detail` list) | Request body failed validation: missing required field, wrong type, unknown message role. | Fix the request. | | 429 | `rate_limited` | Too fast: requests per minute, tokens per minute, or concurrent requests. | Wait and retry, honoring `Retry-After` (seconds, always ≥ 1). See [Rate limits](https://docs.mindshub.ai/inference/rate-limits.md). | | 429 | `included_allowance_exhausted` | Your weighted allowance is used up and no wallet capacity is available. An organization whose included allowance is zero (never granted, or set to zero) gets this too, with no `reset_at`, because nothing refills. | Wait for `reset_at` in the body or `X-MindsHub-Reset-At` in the headers, or add credit. When neither is present, only credit helps. No `Retry-After` is sent. Carries `x-should-retry: false`, so the OpenAI and Anthropic SDKs return it on the first attempt. | | 429 | `free_air_daily_spend_fuse_exceeded` | Free serving is paused for everyone until the daily budget resets. Your own allowance may be untouched. | Wait for `reset_at`, which is the next UTC midnight, or add credit to continue immediately. No `Retry-After` is sent. Carries `x-should-retry: false`, so the OpenAI and Anthropic SDKs return it on the first attempt. | | 4xx (relayed) | varies | The upstream provider rejected something the platform forwarded, for example a `temperature` the model doesn't accept. Body has `"type": "api_error"` and the provider's own message and status. | Fix the request for that model, or switch models. | | 5xx (relayed) | varies | The upstream provider failed. Body has `"type": "api_error"`. The platform retries transient provider errors (and fails over where a fallback route exists) before you see this — except where the response carries `x-should-retry: false`, which marks a failure retrying cannot change. | Retry with backoff, unless `x-should-retry: false` is set. | | 504 | `assembly_timeout` | A non-streaming request ran past its 110-second limit. The limit covers the whole request, failover attempts and web-search turns included, because the edge in front of the API closes a connection that sends nothing for 125 seconds. It usually follows a long reasoning run or a long answer. If MindsHub was running a search or MCP loop for the request (the External search loop and `our client` Remote MCP columns under [Providers](https://docs.mindshub.ai/inference/capability-matrix.md#providers)), the turns, searches and MCP calls that finished before the cut are metered. On Claude models with a large `max_tokens`, the prompt is metered, but not the output generated before the cut. Other timed-out calls record no tokens. | **Don't retry**: it will time out the same way, and may bill again. Resend with `"stream": true`, which is not held to this limit (see [Streaming](https://docs.mindshub.ai/inference/streaming.md#long-running-requests)), or lower `max_tokens` (Claude, Fireworks-hosted models) or `reasoning_effort` (Grok, GPT). | | 502 | none | Couldn't reach the upstream provider. | Retry with backoff. | | 503 | `policy_unavailable` | MindsHub couldn't verify your account's access, so the request was refused instead of run. Transient. | Retry in a few seconds. | | 500 | none (`detail`) | Unhandled internal error. | Retry once; if it persists, report it to [support@mindsdb.com](mailto:support@mindsdb.com). | **The 502 and 504 rows above arrive as a 503.** The network edge in front of the gateway replaces a 502 or 504 with its own error page, discarding the body and headers, so the gateway sends them as a 503 instead, with the original status in `error.upstream_status` (at the top level of the body when there is no `error` object). A timed-out request therefore arrives as a `503` with `code: assembly_timeout`, `upstream_status: 504` and `x-should-retry: false`, and a relayed provider 502 or 504 arrives as a `503` carrying that status in `upstream_status`. A 502 or 504 you do receive came from infrastructure in front of the gateway, not from the gateway itself. The OpenAI and Anthropic SDKs raise the same exception for all three statuses and retry them the same way. **The three 429s mean different things.** `rate_limited` means slow down and retry after seconds. `included_allowance_exhausted` means your own allowance is gone; wait until `reset_at` or top up. If it arrives with no `reset_at`, your organization's included allowance is zero, so waiting will not help and adding credit will. `free_air_daily_spend_fuse_exceeded` means free serving is paused for everyone until the next UTC day, so retrying sooner will not help and adding credit will. Only the rate-limit response sends `Retry-After`. The other two send `x-should-retry: false`, so an OpenAI or Anthropic SDK retries `rate_limited` on its own and hands you the other two on the first attempt. ### The `X-MindsHub-*` headers Denials from access checks carry machine-readable headers that survive even if an intermediary rewrites the response body: | Header | Sent on | Value | |---|---|---| | `X-MindsHub-Reason` | `invalid_credentials`, `permission_denied`, `model_not_found`, `wallet_empty`, `included_allowance_exhausted`, `free_air_daily_spend_fuse_exceeded`, `rate_limited`, `policy_unavailable` | The denial reason. Usually matches the body `code`, except `model_not_found`, where the header reads `unknown_model`. | | `X-MindsHub-Deny-Detail` | `permission_denied` for a model an administrator in your organization restricted | `model_restricted`. Same value as the body's `error.deny_detail`. Absent on every other `permission_denied`. | | `Retry-After` | `rate_limited` only | Seconds to wait, always ≥ 1. | | `X-MindsHub-Reset-At` | `included_allowance_exhausted`, `free_air_daily_spend_fuse_exceeded` | Same ISO-8601 instant as the body-level `reset_at`: when your allowance refills, or when free serving resumes. Absent on `included_allowance_exhausted` when your organization's included allowance is zero. | | `X-MindsHub-Recovery-Url` | `wallet_empty` | Console path for adding credit. Not sent on `model_restricted`, which credit cannot fix. | | `x-should-retry` | `included_allowance_exhausted`, `free_air_daily_spend_fuse_exceeded`, `assembly_timeout` | `false`: the status is one an SDK would retry, but retrying fails the same way. The OpenAI and Anthropic SDKs check it before the status. | Two more `X-MindsHub-*` headers travel on **successful** responses rather than errors, reporting how a request was adapted to the target model. (`X-MindsHub-Failover` and `X-MindsHub-Attempts`, which say whether a fallback served the request, are on both, except on the stream described below; see [Failover](https://docs.mindshub.ai/inference/models.md#failover).) | Header | Sent on | Value | |---|---|---| | `X-MindsHub-Dropped-Params` | Any request carrying a parameter the model can't take | Comma-separated parameter names, e.g. `temperature,top_p` | | `X-MindsHub-Clamped-Params` | Any request carrying a parameter outside the model's range | `name=requested>applied`, e.g. `max_tokens=200000>128000` | Neither appears when nothing was adapted, so their absence means the request went upstream as you wrote it, except on the stream described next. This is why a parameter a model dislikes usually produces a 200 rather than a 400; see [Chat completions](https://docs.mindshub.ai/inference/chat-completions.md#request-parameters). A rewritten `tool_choice` is reported here too, as a clamp: `tool_choice=required>auto` ([why the rewrite happens](https://docs.mindshub.ai/inference/tool-calling.md#controlling-which-tool-runs)). **A stream whose `200` went out early carries none of the `X-MindsHub-*` headers.** When a streamed response takes more than 90 seconds to start, its `200` goes out before the response exists (see [Streaming](https://docs.mindshub.ai/inference/streaming.md#long-running-requests)), so no `X-MindsHub-*` header is sent, whatever was adapted, whether a fallback served, or whether a `previous_response_id` chain was cut short. On such a stream their absence says nothing, and nothing in the body reports the adaptations or a cut chain either. You can tell such a stream apart: it is a `text/event-stream` response without `X-MindsHub-Failover`, a header every other stream carries. Its failover signal still arrives in the body, as the `mindshub` field of its last event, a `response.failed` on Responses included. A Chat Completions or Messages stream that fails after its early `200` reports no failover signal at all (see [Failover](https://docs.mindshub.ai/inference/models.md#failover)). One more gap to code around: request-validation errors (`max_tokens_exceeded`, `model_not_configured`) carry none of these headers. Everywhere else a denial keeps its `Retry-After` and its `X-MindsHub-*` headers, `/v1/messages` included, so back off on the value we send rather than guessing. See [Anthropic compatibility → Errors](https://docs.mindshub.ai/inference/messages.md#errors) for that endpoint's envelope quirks. ### Streaming failures A request with `"stream": true` can fail three ways: 1. **Before any output**: you get a normal JSON error response (any of the above) instead of an SSE stream. Check the response's `Content-Type` before parsing it as SSE. The exception is a response that took more than 90 seconds to start, which gets its `200` early to keep the connection open. A failure after that arrives inside the stream: a `data:` frame with an `error` object on Chat Completions, `response.created` then `response.failed` on Responses, and an `error` event on Messages. Your SDK does not retry a failure that arrives this way, and the failure comes without its own HTTP status or headers. Retry it yourself where you would have retried that failure's status, and tell which failure it is from the `error` object on Chat Completions, `response.error.code` on Responses, or `error.type` on Messages. 2. **Mid-stream, visibly**: the stream ends abnormally. There is no error event. On the OpenAI-compatible endpoints, a stream that ends without ever delivering a `finish_reason` chunk was a failed generation. 3. **Mid-stream, invisibly**: on `/v1/messages`, a failed generation can arrive looking like a **normal completion**: the stream closes with an ordinary `message_delta` (`stop_reason: "end_turn"`) and `message_stop`, just with truncated or empty content. There is currently no wire-level signal for this case. If your application must detect it, sanity-check the output (for example, empty content on a prompt that should produce text). One model-family caveat for rule 2: streams on Gemini-served models (`gemini`, `gemini-flash`) always end with a `finish_reason`, even on failure, so on those, like on `/v1/messages`, absence of `finish_reason` can't be your only health check. Partial output that arrived before a failure is real output and is billed. ### A retry recipe Using plain HTTP (the `requests` library) so the status and headers are visible; SDK users get the same decisions from their SDK's status-code exceptions: ```python import random import time import requests class OutOfTokens(Exception): pass class RequestFailed(Exception): pass RETRYABLE = {429, 500, 502, 503, 504} def server_says_dont_retry(response): # Some failures carry a retryable status but cannot be fixed by retrying. # An `assembly_timeout` re-runs the same generation and bills again, and # the two billing 429s meet the same limit again. Both the Anthropic and # OpenAI SDKs check this header for the same reason. return response.headers.get("x-should-retry") == "false" def exhausted_allowance(response): # Prefer the header; fall back to the body's error code # (the header isn't sent on every endpoint or error). if response.headers.get("X-MindsHub-Reason") == "included_allowance_exhausted": return True try: return response.json().get("error", {}).get("code") == "included_allowance_exhausted" except ValueError: return False def post_with_retries(url, headers, body, max_attempts=5, timeout=120): for attempt in range(max_attempts): try: response = requests.post(url, headers=headers, json=body, timeout=timeout) except requests.RequestException: # Connection failure or timeout: the request may still have run # server-side. See the note on duplicates below. time.sleep(min(2 ** attempt, 30) + random.random()) continue if response.ok: return response if exhausted_allowance(response): raise OutOfTokens(response.headers.get("X-MindsHub-Reset-At")) if response.status_code not in RETRYABLE or server_says_dont_retry(response): raise RequestFailed(response.status_code, response.text) retry_after = response.headers.get("Retry-After") delay = float(retry_after) if retry_after else min(2 ** attempt, 30) time.sleep(max(delay, 1.0) + random.random()) raise RequestFailed("gave up", max_attempts) ``` The decisions that matter: retry `429 rate_limited` and 5xx with backoff and jitter; honor `Retry-After` when present; honor `x-should-retry: false`; stop when the allowance is exhausted until `reset_at`, or until you add credit when no `reset_at` is sent; and never blind-retry `401`, `402`, `403`, `404`, `422`, or a caller-caused `400`. This recipe is written for the OpenAI-compatible endpoints. The Anthropic-compatible `/v1/messages` envelope has no `code`, but allowance exhaustion still carries `reset_at` at the top level and `X-MindsHub-Reset-At` in the headers. Use either value to stop retrying until the reset. When neither is sent, your organization's included allowance is zero, and only credit lifts the denial. Both billing 429s carry `x-should-retry: false` on this endpoint too, and a model an administrator restricted carries `error.deny_detail` and `X-MindsHub-Deny-Detail`. **Retrying after a timeout can generate twice.** A request that timed out client-side may still complete server-side and meter, and there is no idempotency key to deduplicate it. For large generations, prefer a generous `timeout` over aggressive retry, and treat a timeout as "outcome unknown", not "failed". --- ## Getting started MindsHub Inference gives you every major model behind **one API key**, reachable through the three APIs developers already use: - **OpenAI Chat Completions**: `POST /v1/chat/completions` - **OpenAI Responses**: `POST /v1/responses` - **Anthropic Messages**: `POST /v1/messages` These are three request formats over the same engine, not three products. Streaming, tool calling, structured output, vision, and web search work through all of them, and **any model works behind any of the three**: Claude Fable from the OpenAI SDK, GPT 5.6 Sol from the Anthropic SDK. You keep the SDK and the code you already have, and change a base URL. ### 1. Get a key 1. Sign up at [console.mindshub.ai](https://console.mindshub.ai). 2. Create an API key. 3. Copy it when it appears: the full key is shown once, at creation. ```bash export MINDSHUB_API_KEY="mdb_..." ``` New organizations get a recurring weighted allowance on `mindshub_air`, so their first calls cost nothing. Paid requests draw a prepaid wallet. [Jev decisions](https://docs.mindshub.ai/inference/decision-models.md) use a separate endpoint and are priced at $0 during the launch promotion. Until your organization has credit, or a card with a completed top-up and no payment error, Jev has a free daily allowance while shared free capacity lasts. See [Billing](https://docs.mindshub.ai/inference/billing.md#jev-decisions). The samples in these docs use `mindshub_air` wherever the choice of model is not the point, so you can run them before you add a card. Where a sample needs a different model, it says so at that sample. ### 2. The same request, three ways One prompt, sent through all three chat APIs. The Chat Completions tab uses `mindshub_air`, the model your included allowance covers. The other two name other vendors' models to show that any chat model works behind any chat API: Kimi K3 through the OpenAI SDK, GPT 5.6 Sol through the Anthropic SDK. Those two draw the prepaid wallet, so add funds first, or change the model to `mindshub_air` to use the allowance. ```python Chat Completions from openai import OpenAI import os client = OpenAI( base_url="https://api.mindshub.ai/v1", api_key=os.environ["MINDSHUB_API_KEY"], ) response = client.chat.completions.create( model="mindshub_air", # covered by your included allowance messages=[{"role": "user", "content": "Name three uses for a paperclip."}], ) print(response.choices[0].message.content) ``` ```python Responses 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="kimi", # Kimi K3, via the OpenAI SDK input="Name three uses for a paperclip.", ) print(response.output_text) ``` ```python Messages import anthropic import os client = anthropic.Anthropic( base_url="https://api.mindshub.ai", # host only, no /v1 auth_token=os.environ["MINDSHUB_API_KEY"], # auth_token, not api_key ) message = client.messages.create( model="gpt", # GPT 5.6 Sol, via the Anthropic SDK max_tokens=1024, messages=[{"role": "user", "content": "Name three uses for a paperclip."}], ) print(message.content[0].text) ``` ```typescript Chat Completions import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.mindshub.ai/v1", apiKey: process.env.MINDSHUB_API_KEY, }); const response = await client.chat.completions.create({ model: "mindshub_air", // covered by your included allowance messages: [{ role: "user", content: "Name three uses for a paperclip." }], }); console.log(response.choices[0].message.content); ``` ```typescript Responses 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: "kimi", // Kimi K3, via the OpenAI SDK input: "Name three uses for a paperclip.", }); console.log(response.output_text); ``` ```typescript Messages import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.mindshub.ai", // host only, no /v1 authToken: process.env.MINDSHUB_API_KEY, // authToken, not apiKey }); const message = await client.messages.create({ model: "gpt", // GPT 5.6 Sol, via the Anthropic SDK max_tokens: 1024, messages: [{ role: "user", content: "Name three uses for a paperclip." }], }); console.log(message.content[0]); ``` Two details worth remembering: - The **OpenAI SDKs** take the base URL **with** `/v1` and authenticate with `api_key`. - The **Anthropic SDKs** take the base URL **without** `/v1` (the client appends the path itself) and authenticate with **`auth_token`, not `api_key`**. The `api_key` field sends an `x-api-key` header, which MindsHub rejects. The TypeScript samples use top-level `await`, so run them as ES modules: a `.mts`/`.mjs` file, or `"type": "module"` in your `package.json`. Every other sample in these docs assumes a `client` built like one of the six above. Not sure which API to use? [Choosing an API](https://docs.mindshub.ai/inference/choosing-an-api.md) has a one-screen decision table. Prefer curl? The [one-minute version](/#the-one-minute-version) on the landing page has it. ### 3. Switch models by editing one string Every model is addressed by a short, stable **alias**. The alias resolves server-side to a concrete model at the provider, so upgrades don't break your code. The five aliases in this loop all draw the prepaid wallet, because your included allowance covers `mindshub_air` only. ```python for model in ["fable", "gpt", "kimi", "deepseek-v4-flash", "gemini-flash"]: response = client.chat.completions.create( model=model, messages=[{"role": "user", "content": "Explain a hash map in one sentence."}], ) print(f"{model:14} {response.choices[0].message.content}") ``` That loop runs five models from five vendors on one key and one bill. The full list, with reasoning-effort support and live availability, is in [Models](https://docs.mindshub.ai/inference/models.md) and from `GET /v1/models`; what each model supports is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md). ### 4. Go further Each guide shows one feature on all three APIs, with Python and TypeScript samples that run as pasted. Tokens as they are generated, in each API's native event format. Declare functions, receive calls, return results. A JSON schema in, JSON that matches it out. How hard a model thinks, and what that costs. Send images as data URLs or public URLs. Give the model the web, with no search API to sign up for. ### 5. See what it cost Every non-streaming response carries usage; streams end with a usage chunk when you ask via `stream_options: {"include_usage": true}`: ```python print(response.usage.prompt_tokens, response.usage.completion_tokens) ``` And your account-wide totals are one call away: ```bash curl "https://auth.mindshub.ai/v1/usage/summary/?range=period&group_by=model" \ -H "Authorization: Bearer $MINDSHUB_API_KEY" ``` That returns per-model token counts and cost for the current billing period, covering every SDK and coding agent you've pointed at MindsHub. ### What we adapt for you Models disagree about parameters: some take no `top_k`, some take no `reasoning_effort`, and output ceilings differ. A parameter the target model can't take is dropped, a value above its range is clamped down, and the request is served either way, with every change named in the `X-MindsHub-Dropped-Params` and `X-MindsHub-Clamped-Params` response headers (except on a stream whose `200` went out early; see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers)). A model can still restrict the values of a parameter it supports; that error passes through as a 400 (Kimi K3 takes `temperature` only at `1`). The full contract is in [Core concepts](https://docs.mindshub.ai/inference/concepts.md#parameters-are-adapted-not-rejected). ### Use it in your coding agent The same key runs your terminal and editor agents. [Claude Code](https://docs.mindshub.ai/inference/coding-agents.md#claude-code) points at MindsHub with two environment variables and can run on Kimi K3 or any other catalog model, billed to the same balance as your application traffic. [Codex](https://docs.mindshub.ai/inference/codex.md) runs on the same key and the same catalog. ### Where to next Completions, Responses, or Messages, and what each one supports. Claude Code and Codex on any MindsHub model. Every feature and parameter, per API and per model. Aliases, funding, and how routing works. The complete parameter reference. Included allowance, the wallet, and prices. --- ## Images Attach an image to a user message and the model reads it alongside the text. Each API has its own content-part spelling: `image_url` parts on Chat Completions, `input_image` on Responses, `image` blocks on Messages. ### Works with Image parts are accepted on every chat model. The catalog does not 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 Claude, GPT, Gemini, and Grok families read images today, and so does `mindshub_blaze` (Qwen 3.8 27B on Cerebras). The per-model column is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models). **`mindshub_blaze` accepts the same image shapes as every other model, but its provider does not.** Cerebras takes images only as base64 PNG or JPEG data URLs and rejects remote URLs and `detail`. So on this alias the gateway does that work for you: a public `http(s)` URL is downloaded server-side (up to 6 MiB per image), any other raster type (GIF, WebP, …) is transcoded to PNG, and `detail` is dropped before the request goes upstream. Your request shape does not change by model. What you can hit: an image URL that can't be fetched, is over the size cap, or isn't a decodable image returns a `400` naming that URL, as does one whose host resolves to a private or internal address or that redirects to one; Cerebras itself caps a request at 10 images (paid tiers) and 10 MiB total. ### Request ```python Chat Completions response = client.chat.completions.create( model="sonnet", messages=[{ "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, {"type": "image_url", "image_url": {"url": "https://example.com/chart.png"}}, ], }], ) print(response.choices[0].message.content) ``` ```python Responses response = client.responses.create( model="sonnet", input=[{ "role": "user", "content": [ {"type": "input_text", "text": "What's in this image?"}, {"type": "input_image", "image_url": "https://example.com/chart.png"}, ], }], ) print(response.output_text) ``` ```python Messages message = client.messages.create( model="sonnet", max_tokens=1024, messages=[{ "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, {"type": "image", "source": {"type": "url", "url": "https://example.com/chart.png"}}, ], }], ) print(message.content[0].text) ``` ```typescript Chat Completions const response = await client.chat.completions.create({ model: "sonnet", messages: [{ role: "user", content: [ { type: "text", text: "What's in this image?" }, { type: "image_url", image_url: { url: "https://example.com/chart.png" } }, ], }], }); console.log(response.choices[0].message.content); ``` ```typescript Responses const response = await client.responses.create({ model: "sonnet", input: [{ role: "user", content: [ { type: "input_text", text: "What's in this image?" }, { type: "input_image", image_url: "https://example.com/chart.png", detail: "auto" }, ], }], }); console.log(response.output_text); ``` ```typescript Messages const message = await client.messages.create({ model: "sonnet", max_tokens: 1024, messages: [{ role: "user", content: [ { type: "text", text: "What's in this image?" }, { type: "image", source: { type: "url", url: "https://example.com/chart.png" } }, ], }], }); console.log(message.content[0]); ``` These samples use `sonnet`, which reads images and draws the prepaid wallet. Image sources: - A `data:` URL (`data:;base64,`) or a public `http(s)` URL, which the upstream provider fetches. On Messages, `base64` and `url` sources both work. - 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`. ### Response Nothing about the response shape changes: the model's description arrives as ordinary text. Image tokens are metered as input tokens. ### Per-model differences - Anthropic's Files API references (`"source": {"type": "file"}`) are unsupported on Messages, and behavior is target-dependent: some providers reject them, others ignore them. Use `base64` or `url` sources. - An image inside a tool result (a `function_call_output` on Responses, a `tool_result` on Messages, a `tool` message on Chat Completions) reaches the model as an image where it can take one. `glm`, `gpt-oss-fireworks` and `mindshub_blaze` cannot, so they see a short text note in its place instead of failing the request. An image in a user turn is still sent as is. - PDF and other file inputs are not a documented feature yet; a `file` part reaches only the OpenAI-transport models today and is dropped elsewhere. ### Errors you can hit `400` for a malformed data URL or invalid base64. A model without vision returns its provider's own error, relayed with the original status. See [Errors](https://docs.mindshub.ai/inference/errors.md). --- ## MindsHub Inference **Chat, embeddings, and decisions, on one MindsHub key and one bill.** Call Claude, GPT, Gemini, Kimi, DeepSeek, Qwen, Grok and more from a single endpoint, using whichever request format your code already speaks. Same key, same models, same features through all three, and **any chat model works behind any of these three chat APIs**: drive Claude Fable from the OpenAI SDK, or GPT 5.6 Sol from the Anthropic SDK, and neither client knows the difference. `POST /v1/chat/completions`. OpenAI SDKs and most of the ecosystem. Start here. `POST /v1/responses`. OpenAI SDKs, server-side conversation state, Codex. `POST /v1/messages`. Anthropic SDKs and Claude Code. For classification, scoring, and yes/no judgments, use [Jev decision models](https://docs.mindshub.ai/inference/decision-models.md) through `POST /v1/decisions`. Start with the [three-question example](https://docs.mindshub.ai/inference/decisions.md#quick-example). ### The one-minute version Get a key at [console.mindshub.ai](https://console.mindshub.ai), export it as `MINDSHUB_API_KEY`, and point the SDK you already use at `https://api.mindshub.ai/v1`. New organizations get a recurring weighted allowance on `mindshub_air`, so their first calls cost nothing. ```python Python from openai import OpenAI import os client = OpenAI(base_url="https://api.mindshub.ai/v1", api_key=os.environ["MINDSHUB_API_KEY"]) response = client.chat.completions.create( model="mindshub_air", messages=[{"role": "user", "content": "Hello"}], ) print(response.choices[0].message.content) ``` ```typescript TypeScript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.mindshub.ai/v1", apiKey: process.env.MINDSHUB_API_KEY }); const response = await client.chat.completions.create({ model: "mindshub_air", messages: [{ role: "user", content: "Hello" }], }); console.log(response.choices[0].message.content); ``` The same call in curl, and the same request through all three APIs, is in [Getting started](https://docs.mindshub.ai/inference/getting-started.md). ### Start here Key to working app: one request, three ways, then switch models by editing one string. Choose between chat, embeddings, and decisions, then pick a request format. The catalog and its aliases. Every feature and parameter, per API and per model, generated from the code. ### Guides The [decision-model guide](https://docs.mindshub.ai/inference/decision-models.md) explains Jev and its typed answers. The chat guides below show each feature across the three chat APIs. ### Why teams use it **Keep your SDK.** No MindsHub SDK to learn, no wrapper to install. Point the official `openai` or `@anthropic-ai/sdk` client at our base URL and your existing code runs. **Switch models by editing one string.** `"model": "sonnet"` becomes `"model": "kimi"`. No new client, no new credential, no re-plumbing. Benchmark Claude Opus 5.5 against DeepSeek V4.1 Flash on your real prompts in an afternoon. **One bill for everything.** Paid usage draws the same prepaid balance; promotions and included allowances apply as described in [Billing](https://docs.mindshub.ai/inference/billing.md). Check consumption and balance through the [account endpoints](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code), coding agents included. **Chat parameters are adapted.** Chat models disagree about what they accept. Rather than 400-ing because one model dislikes `top_k`, MindsHub drops or clamps what the target model can't take and tells you in a response header. [How that works](https://docs.mindshub.ai/inference/concepts.md#parameters-are-adapted-not-rejected). **Your coding agents come along.** [Claude Code](https://docs.mindshub.ai/inference/coding-agents.md) points at MindsHub with two environment variables and runs on chat models in the catalog. [Codex](https://docs.mindshub.ai/inference/codex.md) does the same with one config block. ### Reference Aliases, passthrough, funding, parameter adaptation. Keys: create, use, revoke. Every error, with retry guidance. Requests, tokens, concurrency. Included allowance, wallet, prices. `POST /v1/embeddings`. --- ## Kimi K3 in Claude Code Claude Code is Anthropic's terminal coding agent. Because MindsHub speaks the [Anthropic Messages format](https://docs.mindshub.ai/inference/messages.md) and resolves any catalog model behind it, Claude Code can run on **Kimi K3**, Moonshot's agentic coding model, without plugins or forks. You change two environment variables (a third is optional), pick the model, and Claude Code's core tools (file edits, shell commands, search) keep working. ### Prerequisites - A MindsHub API key from the [console](https://console.mindshub.ai) ([how keys work](https://docs.mindshub.ai/inference/authentication.md)). - Claude Code installed. See [Anthropic's install docs](https://code.claude.com/docs/en/setup). ### Setup ```bash # Optional: keep MindsHub sessions separate from your claude.ai profile. export CLAUDE_CONFIG_DIR="$HOME/.claude-mindshub" # Host only, no /v1. export ANTHROPIC_BASE_URL="https://api.mindshub.ai" # AUTH_TOKEN, not API_KEY. export ANTHROPIC_AUTH_TOKEN="mdb_your_key_here" claude --model kimi ``` Three things about those variables: - **`CLAUDE_CONFIG_DIR` is optional isolation, not authentication.** `ANTHROPIC_AUTH_TOKEN` outranks a stored claude.ai login, so the setup works without it. The separate directory keeps MindsHub sessions out of your regular profile, and it is the fix if startup still shows the stored login winning (reported on some 2.1.x setups). Run `/status` in a session to see which credential is active. To undo everything, delete the directory and remove the exports. - **`ANTHROPIC_AUTH_TOKEN`, not `ANTHROPIC_API_KEY`.** The API-key variable sends `x-api-key`, which is rejected with a 401 before the request reaches the API. If both are set, the auth token wins. Full rationale in [Anthropic compatibility](https://docs.mindshub.ai/inference/messages.md). - **The base URL is the host only.** Adding `/v1` produces a confusing model error rather than a URL error (see [Troubleshooting](#troubleshooting)). To persist the setup, put the base URL and config directory in your shell profile and supply the key per session; an exported key in a dotfile is plaintext and follows dotfile syncs. A terminal without these variables simply uses your regular claude.ai login. That's the whole integration. `/model kimi` switches a running session; any other [catalog alias](https://docs.mindshub.ai/inference/models.md) works in its place: `gpt`, `gemini-flash`, `sonnet`. ### Verify it One headless call, no session: ```bash claude -p "Reply with exactly: MINDSHUB_OK" --model kimi ``` ``` MINDSHUB_OK ``` Then confirm MindsHub metered it. The row should show `model_alias: "kimi"`: ```bash curl "https://auth.mindshub.ai/v1/usage/summary/?range=period&group_by=model" \ -H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" ``` ### Mix models in one session Claude Code can run its main loop on one model and its subagents on another, all through the same MindsHub key. Kimi K3 orchestrating with Sonnet subagents looks like this: ```bash export ANTHROPIC_MODEL="kimi" export CLAUDE_CODE_SUBAGENT_MODEL="sonnet" ``` `ANTHROPIC_MODEL` sets the session model, same as `--model`. `CLAUDE_CODE_SUBAGENT_MODEL` sends every subagent Claude Code spawns to the given model instead, overriding any `model` set in an agent's own definition; set it to `inherit` to restore normal resolution. Both take a [catalog alias](https://docs.mindshub.ai/inference/models.md) (`kimi`, not `kimi-k3`). Claude Code also keeps a Haiku-class model for background work such as summarizing past sessions for `--resume`. Left alone, that traffic resolves to the `haiku` alias on MindsHub and bills at Claude Haiku rates. Point it at Kimi to keep the whole session on one model: ```bash export ANTHROPIC_DEFAULT_HAIKU_MODEL="kimi" ``` `ANTHROPIC_DEFAULT_SONNET_MODEL` and `ANTHROPIC_DEFAULT_OPUS_MODEL` remap the `sonnet` and `opus` aliases the same way. `ANTHROPIC_SMALL_FAST_MODEL`, which older guides mention, is deprecated in favor of `ANTHROPIC_DEFAULT_HAIKU_MODEL`. A split session meters per model: the [usage summary](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code) shows one row per alias, so orchestrator and subagent costs stay separately visible. ### Cost and token reporting - **Claude Code's cost display is not your bill.** It prices non-Claude models at fallback rates. Kimi K3's real rate is $3.00 in / $15.00 out per million tokens ([price list](https://docs.mindshub.ai/inference/billing.md#price-list)); the [usage summary](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code) is authoritative. - **Kimi thinks before it answers.** K3 reasons internally, and that reasoning bills as output tokens, so a one-line reply can meter a few hundred output tokens: typically well under a cent. There's no effort setting to adjust; the catalog reports no reasoning levels for `kimi` ([why](https://docs.mindshub.ai/inference/models.md#reasoning-effort)). - **Prompt caching works in your favor.** Claude Code caches aggressively, and Kimi's cached reads bill at $0.30 per million instead of $3.00, which is where most of a long session's input lands. Kimi has no separate cache-write charge today ([price list](https://docs.mindshub.ai/inference/billing.md#price-list)). - **Mid-stream token counts read low.** On non-Claude models the stream reports real usage only at the end, so Claude Code's live context meter undercounts until each turn finishes. Display only; the usage summary is what bills. ### Troubleshooting | Symptom | Cause and fix | |---|---| | `401 Authorization Required` at startup, HTML body | The edge rejected your credential. Either a stored claude.ai login is taking precedence (set `CLAUDE_CONFIG_DIR` to a fresh directory), or your key is in `ANTHROPIC_API_KEY` only, which sends the wrong header. Set `ANTHROPIC_AUTH_TOKEN`. | | `There's an issue with the selected model (…)` | Two different causes, and Claude Code shows the same message for both. Check that the model is a catalog alias (`kimi`, not `kimi-k3`; see [`GET /v1/models`](https://docs.mindshub.ai/inference/models.md)), **and** that `ANTHROPIC_BASE_URL` has no `/v1` suffix. | | `API Error: 524` | The edge closed a connection that sent nothing for 125 seconds. Claude Code streams, and a stream now sends keepalives while the model thinks, so this should not recur; if it does, report the time and the response's `cf-ray` header to [support@mindsdb.com](mailto:support@mindsdb.com). The timed-out request may still have completed and billed. | | Replies truncate mid-answer | Kimi's internal reasoning used the output budget before the visible answer. Set `CLAUDE_CODE_MAX_OUTPUT_TOKENS=8192` (or higher) and restart. | *Verified with Claude Code 2.1.220 in August 2026: setup, marker prompts on `kimi` and other aliases, a split session (Kimi orchestrator, Sonnet subagent) metering as separate usage rows, and the `haiku` alias remapped to `kimi`.* --- ## Messages(2) `POST /v1/messages` implements the Anthropic Messages API shape, so Anthropic SDKs and the tools built on them, most notably **Claude Code**, run against MindsHub with a base-URL and auth-token change. Model names resolve through the same catalog as everywhere else, so you can point an Anthropic client at *any* MindsHub model: `gpt`, `kimi`, `gemini-flash`, not just Claude. ### Endpoint `POST https://api.mindshub.ai/v1/messages`, plus `POST /v1/messages/count_tokens`. Anthropic SDKs take the base URL **without** `/v1` (the client appends `/v1/messages` itself) and must authenticate with **`auth_token`, not `api_key`**. The `api_key` field sends an `x-api-key` header, which MindsHub rejects with a 401 before the request lands. Install the official client: `pip install anthropic` or `npm install @anthropic-ai/sdk`. Running Claude Code against MindsHub is covered in [Coding agents](https://docs.mindshub.ai/inference/coding-agents.md#claude-code). ### Quick example ```python Python import os import anthropic client = anthropic.Anthropic( base_url="https://api.mindshub.ai", auth_token=os.environ["MINDSHUB_API_KEY"], # auth_token, not api_key ) message = client.messages.create( model="mindshub_air", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], ) print(message.content[0].text) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.mindshub.ai", authToken: process.env.MINDSHUB_API_KEY, // authToken, not apiKey }); const message = await client.messages.create({ model: "mindshub_air", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }], }); console.log(message.content[0]); ``` The `anthropic-version` header your SDK sends is accepted and ignored. ### Request parameters | Parameter | Handling | Notes | |---|---|---| | `model` | adapted | A real Claude model id is family-mapped to the catalog alias serving that family (claude-sonnet-5 -> sonnet); a bare alias resolves as-is. The response names the model that ran; `mindshub_air` and `mindshub_blaze` echo the id the caller sent. | | `messages` | conditional | Text, image, tool_use, tool_result and thinking blocks all round-trip to a Claude-family target, where is_error and block-level cache_control survive too. Against another provider each block is rebuilt into that transport's own vocabulary, and anything it has no field for — an Anthropic-only block type, a cache_control or citations marker — is dropped and named in X-MindsHub-Dropped-Content rather than forwarded into a 400. | | `system` | conditional | String or content blocks. Blocks pass through verbatim so cache_control markers reach a Claude-family target; against another provider they are reconciled like any other content, and what that transport cannot carry is named in X-MindsHub-Dropped-Content. | | `stream` | forwarded | Produces the standard Anthropic event sequence. See the module docstring for the two documented divergences (ping is sent once, and input tokens are 0 on message_start for non-Claude targets). | | `max_tokens` | conditional | Clamped to the resolved model's real output ceiling, and up to its floor where the backend enforces one; either adjustment is named in X-MindsHub-Clamped-Params. | | `temperature` | conditional | Dropped on models that reject sampling params — which is every current Claude 5 model — and named in X-MindsHub-Dropped-Params. | | `top_p` | conditional | As temperature. On a Claude model that still takes sampling params, sending it with temperature drops top_p (Anthropic rejects the pair) and names it in X-MindsHub-Dropped-Params; top_p alone is forwarded. | | `top_k` | conditional | As temperature. | | `stop_sequences` | conditional | Forwarded where the transport has a field for them (the OpenAI Responses transport does not). A sequence that fires is reported as stop_reason 'stop_sequence' with the matched string, on Anthropic-wire targets. | | `output_config` | adapted | Both halves are honoured and travel separately: 'effort' is clamped onto the resolved model's published ladder, and 'format' becomes a schema constraint each provider renders in its own shape. A model that cannot constrain its output at all is a 400, not prose. | | `thinking` | conditional | Forwarded to models that accept a thinking config, budget_tokens included. Reasoning bills as output tokens. Thinking blocks come back on both streamed and non-streamed turns, so a client can replay them. | | `tools` | adapted | Client tools (input_schema) translate to every provider, strict included. Anthropic's hosted web_search*/web_fetch* map onto the platform's own web search where the target supports it. An mcp_toolset (with its mcp_servers entry) maps onto the target's own remote-MCP connector where it has one and is dropped, named in X-MindsHub-Dropped-Params, where it does not; enabled/default_config.enabled are honoured, defer_loading is dropped. Other server tools (computer_*, bash_*, text_editor_*, code_execution, memory) have no cross-provider meaning and are dropped. A tool schema is rewritten where the target accepts only a subset of JSON Schema; both kinds of change are named in X-MindsHub-Dropped-Content. | | `tool_choice` | adapted | auto/any/none/tool all map. disable_parallel_tool_use is honoured, including on transports that spell it as a positive parallel_tool_calls. A forced choice a model cannot honour is downgraded to auto rather than refused (ENG-1095). | | `mcp_servers` | adapted | Remote MCP servers, Anthropic's shape (type url, url, name, authorization_token). Each must be referenced by exactly one mcp_toolset in tools, as on Anthropic; otherwise 400. Runs on the target's native connector (Claude, GPT); the authorization token is forwarded upstream and never stored or traced. This dialect has no approval concept, so tools run as the model calls them on every target. | | `metadata` | ignored | Accepted and not read. | | `service_tier` | ignored | One serving tier; nothing to select. | | `cache_control` | ignored | The top-level auto-caching form is not read. Block-level cache_control on system blocks, text/tool_result blocks and tool definitions IS honoured on Claude-family targets, and cache usage is reported in the response's usage. | | `container` | ignored | The code-execution tool is not offered here. | | `inference_geo` | ignored | Inference geography is a property of the upstream account, not of a request. | *This table is generated from `PARAM_SUPPORT` in `minds/requests/anthropic_compat.py`, which a unit test holds to the vendor SDK's own parameter list. The cross-API view is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#request-parameters-by-api).* Features the table can only summarize have their own guides: [Tool calling](https://docs.mindshub.ai/inference/tool-calling.md) (`tools`, `tool_choice`, `disable_parallel_tool_use`), [Structured output](https://docs.mindshub.ai/inference/structured-output.md) (`output_config.format`), [Reasoning](https://docs.mindshub.ai/inference/reasoning.md) (`output_config.effort`, `thinking`), [Web search](https://docs.mindshub.ai/inference/web-search.md) (`web_search*` and `web_fetch*` hosted tools), [Prompt caching](https://docs.mindshub.ai/inference/prompt-caching.md) (block-level `cache_control`), and [Images](https://docs.mindshub.ai/inference/images.md) (`image` blocks). Server tools (`computer_*`, `bash_*`, `text_editor_*`, `code_execution`, `memory`) are dropped from the request; `mcp_servers` is ignored (Claude Code's own MCP support is client-side and unaffected); Files API references (`"source": {"type": "file"}`) are unsupported; the Message Batches API is not implemented and `POST /v1/messages/batches` returns a plain 404. Unknown **top-level** request fields are accepted and ignored, not a validation error. Unknown *nested* content, say an unrecognized block type inside `messages`, can be forwarded to the provider and fail there instead. The cross-API view of every parameter is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#request-parameters-by-api). #### Model names The `model` field accepts two forms: - **A MindsHub alias**: `sonnet`, `opus`, `gpt`, `kimi`, any alias from [Models](https://docs.mindshub.ai/inference/models.md). This is how you run Claude Code on a non-Claude model. - **A real Claude model name**: anything starting with `claude` that contains a family name (`opus`, `sonnet`, `haiku`, or `fable`; a substring match, checked in that order) maps to the corresponding alias. `claude-sonnet-5`, `claude-sonnet-4-6-20250929`, and `claude-opus-5[1m]` all work; version and date segments are ignored, and the family decides the alias. This is what lets Claude Code's own model picker work unmodified. A `claude…` name with no recognizable family word returns `404 model_not_found`; there is no silent guessing. ### Request body The full request and response schemas, with every field described, are on the generated reference pages: [POST /v1/messages](/api-reference/create-message) and [POST /v1/messages/count_tokens](/api-reference/count-message-tokens). `mcp_servers` (Anthropic's beta remote-MCP field, paired with `mcp_toolset` entries in `tools`) is accepted on every model and runs on the target's own connector; see [Remote MCP servers](https://docs.mindshub.ai/inference/tool-calling.md#remote-mcp-servers). The `anthropic-beta: mcp-client-2025-11-20` header the Anthropic SDK sends with it is accepted and not required. ### Response ```json { "id": "msg_5084f82319564e568c6e82fd8520c125", "type": "message", "role": "assistant", "model": "claude-sonnet-5", "content": [{"type": "text", "text": "Hello!"}], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 11, "output_tokens": 4, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0 } } ``` - `model` names the model that served, as every endpoint does, so it can differ from the Claude id you sent. `mindshub_air` and `mindshub_blaze` echo exactly the string you sent. See [Models](https://docs.mindshub.ai/inference/models.md#what-the-responses-model-field-contains). - `content` blocks are `text`, `tool_use`, and, on models that think by default (`opus`, `fable`), a leading `thinking` block with a `signature`. A turn that called a [remote MCP server](https://docs.mindshub.ai/inference/tool-calling.md#remote-mcp-servers) also carries `mcp_tool_use` / `mcp_tool_result` pairs ahead of the text, on Claude and GPT targets alike (non-streamed turns only). Don't assume the first block is your text. Thinking blocks come back on streamed and non-streamed turns alike, in Anthropic's required order. - `stop_reason` is one of `end_turn`, `tool_use`, `max_tokens`, `stop_sequence`, `refusal`. On a Claude-family target the model's own word is reported, so a stop your `stop_sequences` triggered comes back as `stop_sequence` with the matched string in `stop_sequence`, and a refusal carries `stop_details`. On other targets the upstream only tells us as much as OpenAI's `finish_reason` does, so `stop_sequence` is not distinguishable from `end_turn` there. - **Beta-only stop reasons are mapped onto stable ones.** `model_context_window_exceeded` and `compaction` arrive as `max_tokens`; treat the response as truncated. `pause_turn` (a server-tool loop that hit its iteration cap) is continued for you on a non-streamed turn, up to three times; if the turn is still paused after that and the response body carries the `mcp_tool_use` blocks the resumption needs, it is reported as `pause_turn` and you re-send the assistant content with the same tools, as on Anthropic. A paused turn whose blocks are not in the body (web search, or any streamed turn) still arrives as `end_turn`. - All four `usage` fields are always present. As in Anthropic's own API, `input_tokens` **excludes** cached tokens: the three input fields partition the prompt. ### Streaming events `"stream": true` produces the standard Anthropic event sequence (`message_start` → `ping` → `content_block_*` → `message_delta` → `message_stop`), with three differences from Anthropic's native stream: `ping` is sent once, `message_start` reports `input_tokens: 0` on non-Claude models, and a failure after the body opens arrives as an `error` event. Details in [Streaming](https://docs.mindshub.ai/inference/streaming.md#messages). ### Lane-specific behaviour #### Counting tokens ```bash curl https://api.mindshub.ai/v1/messages/count_tokens \ -H "Authorization: Bearer $MINDSHUB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "sonnet", "messages": [{"role": "user", "content": "Hello"}]}' ``` ```json {"input_tokens": 8} ``` Counts come from Anthropic's real counter. Name a concrete Claude model (`claude-sonnet-5`) and you get that model's own exact count. Send a catalog alias or a non-Claude model and the count runs against the platform's current Claude counting model instead, so read those as close rather than exact: tokenizers differ by roughly 1.3x across Claude generations, and further still off the family. If Anthropic is unreachable, the endpoint still answers from a rough local estimate rather than failing. Budget with these; don't reconcile billing with them. `count_tokens` calls are free and unmetered. #### Model discovery `GET /v1/models` answers in a shape the Anthropic SDK's `client.models.list()` parses. Each row carries `type`, `display_name`, and `created_at` beside the OpenAI-shaped fields. See [Models endpoint](https://docs.mindshub.ai/inference/models-endpoint.md). ### Errors Errors on this endpoint use the Anthropic envelope: ```json {"type": "error", "error": {"type": "not_found_error", "message": "The model 'foo' does not exist or you do not have access to it."}} ``` The status codes and their meanings are the same as everywhere else; see [Errors](https://docs.mindshub.ai/inference/errors.md). Three quirks specific to this endpoint: - A 402 (empty wallet) arrives typed `invalid_request_error`, and a 503 typed `api_error`. Read the HTTP status, not just the type string. A 429 is typed `rate_limit_error` and carries no `code` field, so a client keying retries on `error.code` alone will miss it. - **Denial headers survive the envelope.** A 429 carries `Retry-After` and the `X-MindsHub-*` headers, so back off on the value we send rather than guessing. The two billing 429s carry `x-should-retry: false`, and a 403 for a model an administrator restricted carries `X-MindsHub-Deny-Detail: model_restricted` plus `error.deny_detail` inside the envelope. The param-adaptation headers travel too, on successful requests and on errors the target model itself returned, except on a stream whose `200` went out early, which carries none of the `X-MindsHub-*` headers (see [Errors](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers)). - A malformed request body returns `400` (message prefixed `Invalid request body:`). *The per-parameter contract this page summarizes is maintained in code, as `PARAM_SUPPORT` in `minds/requests/anthropic_compat.py`, and held to the Anthropic SDK's own parameter list by a test.* --- ## Models endpoint `GET /v1/models` lists every alias in the catalog with the metadata a client needs to build a model picker: label, whether your organization can call it right now, reasoning-effort levels, provider, and family. It is free to call and never paginated. Every model includes `kind`: `chat`, `embedding`, or `decision`. Use it to select the appropriate endpoint and filter chat pickers. Jev decision models use [`POST /v1/decisions`](https://docs.mindshub.ai/inference/decisions.md). ### Endpoint `GET https://api.mindshub.ai/v1/models` ```python Python for model in client.models.list(): print(model.id) ``` ```typescript TypeScript for await (const model of client.models.list()) { console.log(model.id); } ``` The generated reference page is [GET /v1/models](/api-reference/list-models). Both the OpenAI and Anthropic SDKs' `models.list()` parse the response: each row carries `type`, `display_name`, and `created_at` beside the OpenAI-shaped fields, and `has_more` is always `false`. ### Response ```json { "object": "list", "data": [ { "id": "sonnet", "label": "Claude Sonnet", "object": "model", "created": 0, "enabled": true, "reasoning_efforts": ["low", "medium", "high", "xhigh", "max"], "default_reasoning_effort": "high", "embedding": false, "provider": "anthropic", "family": "sonnet", "current_version": "sonnet-5-5", "default_for": [] } ], "has_more": false } ``` | Field | Meaning | |---|---| | `id` | The alias. This is what you put in the `model` field of a request. | | `label` | Human-readable display name. | | `enabled` | Whether your organization can call this model right now. Disabled models still appear in the listing; `disabled_reason` says why. | | `disabled_reason` | Why `enabled` is `false`: `model_restricted` (an administrator in your organization restricted the model, and credit does not help), `wallet_empty` (the model needs wallet credit), or `included_allowance_exhausted` (the included allowance is used up or zero, and no wallet capacity is available). The same words a request for that model would be refused with. Omitted on an enabled row. | | `reasoning_efforts` | Effort levels the model accepts, or `null` if the level isn't adjustable. `null` does **not** mean the model doesn't reason; see [Reasoning](https://docs.mindshub.ai/inference/reasoning.md). | | `default_reasoning_effort` | The effort used when you don't specify one. | | `embedding` | `true` for embedding models (use them with [`/v1/embeddings`](https://docs.mindshub.ai/inference/embeddings.md), not chat). | | `provider` | The provider serving the model (`anthropic`, `openai`, `gemini`, `fireworks`, `cerebras`, `meta`, `xai`, `typesafe`). This is who *serves* the model, not necessarily who trained it. | | `family` | The moving alias this fixed-version or compatibility pin belongs to. `family == id` means this alias tracks the newest version; any other value names the pin's moving head. | | `current_version` | On a moving alias, the fixed alias that serves the same model today: pin it to stay on that model when the moving alias is repointed. Omitted on fixed aliases, and on a moving alias with no same-model pin (`mindshub_air`, `mindshub_blaze`). | | `supported_params` | **Reserved, not published yet.** Generation parameters the model honors, in the platform's parameter names. No row carries it today, so treat its absence as "unknown"; the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models) answers the question in the meantime. | | `default_for` | The agent roles this alias is the default for, for a client that lets a user pick a model per role: any of `planning`, `coding`, `router`. Empty on every alias that is nobody's default. It grants nothing; `enabled` still decides whether you may call the model. | | `created` | Always `0`. Not a real timestamp. | `provider`, `family`, `default_for`, and `disabled_reason` are **omitted** from a row rather than sent as `null` when the service hasn't classified it, so read them with a default. A disabled row with no `disabled_reason` means the service cannot say why. `default_for` is the one where absent and empty differ: `[]` means this alias is nobody's default; a missing key means the service cannot say. There is no `owned_by` field. Code that reads `owned_by` won't find it. ### Client-specific responses The endpoint answers in three dialects, chosen from the request's headers: - **Default**: the shape above. - **Claude Code** (detected from its user agent): ids are minted as `anthropic-`, rows carry `display_name`, and the list is filtered to moving, non-embedding aliases so Claude Code's picker shows one entry per family. See [Coding agents](https://docs.mindshub.ai/inference/coding-agents.md#claude-code). - **Codex**: a `{"models": [...]}` list with `slug`, the client's own `base_instructions`, `visibility` overrides for bundled slugs, and a content-derived `ETag`. See [Codex](https://docs.mindshub.ai/inference/codex.md). ### Errors you can hit `401 invalid_credentials` without a valid key. The listing itself never returns a `402` or `429`: disabled models are listed with `enabled: false` instead. See [Errors](https://docs.mindshub.ai/inference/errors.md). --- ## Models(2) You address models by **alias**: a short, stable name like `sonnet` or `gpt`. The alias resolves server-side to a concrete model on one of the upstream providers. When the underlying model is upgraded, the alias stays the same, so you don't have to chase version strings. Jev uses the [Decisions API](https://docs.mindshub.ai/inference/decisions.md), with native `state` and typed `questions`. Model catalog entries with `kind: "decision"` use `/v1/decisions`; the chat APIs described here apply to `kind: "chat"`. ### Listing models `GET /v1/models` returns the live catalog with per-model metadata: label, whether your organization can call it right now, reasoning-effort levels, provider, and family. The row schema and the client-specific dialects are on the [Models endpoint](https://docs.mindshub.ai/inference/models-endpoint.md) page; what each model supports is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models). ### The catalog Current as of October 2026; `GET /v1/models` is authoritative and changes without notice. Prices are in [Billing](https://docs.mindshub.ai/inference/billing.md#price-list). Three kinds of row, and the `family` field tells them apart: - **A moving alias** (`family == id`) tracks the newest release in its line. Its label names the line — `GPT`, `Claude Sonnet` — never the version it happens to serve, so the name stays put across repoints. What it resolves today can change; when it does, your requests move with it. - **A fixed alias naming the model its moving alias serves today.** `current_version` on the moving alias names it. Pick this one to hold a model through a repoint. - **A fixed alias for an older release.** Also carries `family`, but is not named by its moving alias's `current_version`. Every model we serve is reachable by at least one alias that never moves. | Alias | Label | Notes | |---|---|---| | `mindshub_air` | MindsHub Air | Tracks the newest release. No fixed alias of its own. | | `mindshub_blaze` | MindsHub Blaze | Tracks the newest release. No fixed alias of its own. | | `gpt-oss-fireworks` | GPT-OSS 120B (Fireworks) | Fixed. `mindshub_blaze`'s Fireworks-hosted fallback — a different model (GPT-OSS 120B) than `mindshub_blaze` currently serves. | | `sonnet` | Claude Sonnet | Tracks the newest release. Currently Claude Sonnet 5.5 — pin `sonnet-5-5` to hold that model. | | `sonnet-5-5` | Claude Sonnet 5.5 | Fixed. The same model `sonnet` serves today, and it stays on it when `sonnet` moves. | | `sonnet-5` | Claude Sonnet 5 | Fixed. An older release of `sonnet`. | | `opus` | Claude Opus | Tracks the newest release. Currently Claude Opus 5.5 — pin `opus-5-5` to hold that model. | | `opus-5-5` | Claude Opus 5.5 | Fixed. The same model `opus` serves today, and it stays on it when `opus` moves. | | `opus-5` | Claude Opus 5 | Fixed. An older release of `opus`. | | `fable` | Claude Fable | Tracks the newest release. Currently Claude Fable 5.1 — pin `fable-5-1` to hold that model. | | `fable-5-1` | Claude Fable 5.1 | Fixed. The same model `fable` serves today, and it stays on it when `fable` moves. | | `fable-5` | Claude Fable 5 | Fixed. An older release of `fable`. | | `haiku` | Claude Haiku | Tracks the newest release. Currently Claude Haiku 4.5 — pin `haiku-4-5` to hold that model. | | `haiku-4-5` | Claude Haiku 4.5 | Fixed. The same model `haiku` serves today, and it stays on it when `haiku` moves. | | `gpt` | GPT | Tracks the newest release. Currently GPT-6 Astra — pin `gpt-6-astra` to hold that model. | | `gpt-6-astra` | GPT-6 Astra | Fixed. The same model `gpt` serves today, and it stays on it when `gpt` moves. | | `gpt-5-6-sol` | GPT 5.6 Sol | Fixed. An older release of `gpt`. | | `gpt-sol` | GPT Sol | Tracks the newest release. Currently GPT-6.1 Sol — pin `gpt-sol-6-1` to hold that model. | | `gpt-sol-6` | GPT-6 Sol | Fixed. An older release of `gpt-sol`. | | `gpt-sol-6-1` | GPT-6.1 Sol | Fixed. The same model `gpt-sol` serves today, and it stays on it when `gpt-sol` moves. | | `gpt-terra` | GPT Terra | Tracks the newest release. Currently GPT 5.6 Terra — pin `gpt-terra-5-6` to hold that model. | | `gpt-terra-5-6` | GPT 5.6 Terra | Fixed. The same model `gpt-terra` serves today, and it stays on it when `gpt-terra` moves. | | `gpt-luna` | GPT Luna | Tracks the newest release. Currently GPT-6 Luna — pin `gpt-luna-6` to hold that model. | | `gpt-luna-5-6` | GPT 5.6 Luna | Fixed. An older release of `gpt-luna`. | | `gpt-luna-6` | GPT-6 Luna | Fixed. The same model `gpt-luna` serves today, and it stays on it when `gpt-luna` moves. | | `gpt-codex` | GPT Codex | Tracks the newest release. Currently GPT 5.3 Codex — pin `gpt-codex-5-3` to hold that model. | | `gpt-codex-5-3` | GPT 5.3 Codex | Fixed. The same model `gpt-codex` serves today, and it stays on it when `gpt-codex` moves. | | `gpt-mini` | GPT Mini | Tracks the newest release. Currently GPT 5.4 Mini — pin `gpt-mini-5-4` to hold that model. | | `gpt-mini-5-4` | GPT 5.4 Mini | Fixed. The same model `gpt-mini` serves today, and it stays on it when `gpt-mini` moves. | | `gpt-nano` | GPT Nano | Tracks the newest release. Currently GPT 5.4 Nano — pin `gpt-nano-5-4` to hold that model. | | `gpt-nano-5-4` | GPT 5.4 Nano | Fixed. The same model `gpt-nano` serves today, and it stays on it when `gpt-nano` moves. | | `gemini` | Gemini Pro | Tracks the newest release. Currently Gemini 3.1 Pro Preview — pin `gemini-3-1-pro` to hold that model. | | `gemini-3-1-pro` | Gemini 3.1 Pro Preview | Fixed. The same model `gemini` serves today, and it stays on it when `gemini` moves. | | `gemini-flash` | Gemini Flash | Tracks the newest release. Currently Gemini 3.8 Flash — pin `gemini-flash-3-8` to hold that model. | | `gemini-flash-3-8` | Gemini 3.8 Flash | Fixed. The same model `gemini-flash` serves today, and it stays on it when `gemini-flash` moves. | | `gemini-flash-3-7` | Gemini 3.7 Flash | Fixed. An older release of `gemini-flash`. | | `gemini-flash-3-6` | Gemini 3.6 Flash | Fixed. An older release of `gemini-flash`. | | `gemini-flash-3-5` | Gemini 3.5 Flash | Fixed. An older release of `gemini-flash`. | | `gemini-flash-3` | Gemini 3 Flash Preview | Fixed. An older release of `gemini-flash`. | | `gemini-flash-lite` | Gemini Flash-Lite | Tracks the newest release. Currently Gemini 3.1 Flash-Lite — pin `gemini-flash-lite-3-1` to hold that model. | | `gemini-flash-lite-3-1` | Gemini 3.1 Flash-Lite | Fixed. The same model `gemini-flash-lite` serves today, and it stays on it when `gemini-flash-lite` moves. | | `kimi` | Kimi | Tracks the newest release. Currently Kimi K3 — pin `kimi-k3` to hold that model. | | `kimi-k3` | Kimi K3 | Fixed. The same model `kimi` serves today, and it stays on it when `kimi` moves. | | `deepseek-v4-flash` | DeepSeek Flash | Tracks the newest release. Currently DeepSeek V4.1 Flash — pin `deepseek-v4-1-flash` to hold that model. | | `deepseek-v4-1-flash` | DeepSeek V4.1 Flash | Fixed. The same model `deepseek-v4-flash` serves today, and it stays on it when `deepseek-v4-flash` moves. | | `qwen` | Qwen | Tracks the newest release. Currently Qwen3.8-2.4T-A95B — pin `qwen-3-8-a95b` to hold that model. | | `qwen-3-8-a95b` | Qwen3.8-2.4T-A95B | Fixed. The same model `qwen` serves today, and it stays on it when `qwen` moves. | | `glm` | GLM | Tracks the newest release. Currently GLM 5.3 — pin `glm-5-3` to hold that model. | | `glm-5-3` | GLM 5.3 | Fixed. The same model `glm` serves today, and it stays on it when `glm` moves. | | `glm-5-3-flash` | GLM Flash | Tracks the newest release. Currently GLM 5.3 Flash — pin `glm-flash-5-3` to hold that model. | | `glm-flash-5-3` | GLM 5.3 Flash | Fixed. The same model `glm-5-3-flash` serves today, and it stays on it when `glm-5-3-flash` moves. | | `muse-spark` | Muse Spark | Tracks the newest release. Currently Muse Spark 1.3 — pin `muse-spark-1-3` to hold that model. | | `muse-spark-1-3` | Muse Spark 1.3 | Fixed. The same model `muse-spark` serves today, and it stays on it when `muse-spark` moves. | | `muse-spark-1-2` | Muse Spark 1.2 | Fixed. An older release of `muse-spark`. | | `muse-spark-1-1` | Muse Spark 1.1 | Fixed. An older release of `muse-spark`. | | `grok` | Grok | Tracks the newest release. Currently Grok 4.7 — pin `grok-4-7` to hold that model. | | `grok-4-7` | Grok 4.7 | Fixed. The same model `grok` serves today, and it stays on it when `grok` moves. | | `grok-4-6` | Grok 4.6 | Fixed. An older release of `grok`. | | `grok-4-5` | Grok 4.5 | Fixed. An older release of `grok`. | | `embed-small` | Text Embedding | Tracks the newest release. Currently Text Embedding 3 (small) — pin `embed-3-small` to hold that model. | | `embed-3-small` | Text Embedding 3 (small) | Fixed. The same model `embed-small` serves today, and it stays on it when `embed-small` moves. | | `jev` | Jev | Decision model. Currently `jev-1.13.0`; `jev-latest` is an accepted synonym. Uses `/v1/decisions`. | | `jev-1.13.0` | Jev 1.13.0 | Fixed decision-model version. | ### Alias rules - **Send the bare alias.** `"model": "sonnet"`. - **Provider model IDs work too.** `"model": "claude-sonnet-5"` or `"gpt-5.4-mini"` resolves to the catalog entry serving that model, which is what makes a response's `model` safe to send back. The Anthropic-compatible `/v1/messages` endpoint also maps Claude family names onto aliases so Claude Code works unmodified; see [Messages](https://docs.mindshub.ai/inference/messages.md#model-names). - **`latest:` is deprecated** but still accepted; `latest:sonnet` is exactly `sonnet`. New code should not use it. - **Unknown names return `404`** with error code `model_not_found`, whether the name is a typo, a raw provider ID, or a model that exists but isn't in the catalog. ### What the response's `model` field contains **Chat and embedding endpoints name the model that served the request.** Ask for `haiku` and the response says `claude-haiku-4-5-20251001`. If the requested model was down and a fallback answered, `model` names the fallback. This holds on `/v1/chat/completions`, `/v1/responses`, `/v1/messages` and `/v1/embeddings` alike, on streaming frames as well as whole responses. **`mindshub_air` and `mindshub_blaze` are the exception.** They return the string you sent (`mindshub_air`), fallback included. The model behind them is ours to move, so it is never named. The value is safe to reuse: send it back as the `model` of your next request and it resolves. It resolves to the alias that currently serves that model, **not to a pin**: send back `claude-haiku-4-5-20251001` and you are on `haiku`, which moves when `haiku` is repointed. To hold one model, send its pinned alias from [the catalog](#the-catalog) (`haiku-4-5`). The [Decisions endpoint](https://docs.mindshub.ai/inference/decisions.md#models-and-versions) returns the actual served Jev version instead: requesting `jev` currently returns `jev-1.13.0`. For chat and embeddings, three consequences are worth knowing: - **Log the alias you requested alongside the returned `model`.** The returned value changes when we repoint an alias or a fallback answers, so aggregate on your own requested alias if you want a stable key. - **A fallback is billed at the rate of the alias you requested**, even when `model` names a different model. See [Billing](https://docs.mindshub.ai/inference/billing.md). - **On `mindshub_air` and `mindshub_blaze`, `model` does not reveal a fallback.** Read [the failover signal](#failover) instead. ### Failover When the requested model fails with a retryable error (a timeout, a `429`, most `5xx`), the request is retried on the same model and then handed to a fallback. Every response from chat completions, responses and messages says whether that happened, on success and on error, streamed or not, with one exception described below: | Header | Value | |---|---| | `X-MindsHub-Failover` | `true` when a fallback produced the response, `false` when the requested model did. | | `X-MindsHub-Attempts` | Upstream attempts, requested model and fallbacks together. `1` means the first try answered; `2` with `false` means one retry on the requested model. It counts the platform's own attempts, not retries inside a provider SDK. | The same two values are in the body, for SDKs that only expose headers through a raw-response call: `"mindshub": {"failover": false, "attempts": 1}` at the top level of a whole response, and on the last event of a stream (the final chat chunk before `[DONE]`, the closing Anthropic `message_delta`, or the `response` in `response.completed`, `response.incomplete` or `response.failed`). Error bodies don't carry it; read the headers there. Neither signal names the fallback model or its provider. Both headers are present on every response that reached a model, except the stream described next, so a missing header on any other response means the request was refused before any model ran (a bad key, an exhausted allowance), not that no failover happened. **A stream whose `200` went out early carries neither header.** A streamed request whose response takes more than 90 seconds to start gets its `200` before the response exists (see [Streaming](https://docs.mindshub.ai/inference/streaming.md#long-running-requests)), so it carries none of the `X-MindsHub-*` headers, and a `text/event-stream` response without them is one of these. Its body still reports failover: the `mindshub` field rides its last event as usual, a `response.failed` on Responses included. A Chat Completions or Messages stream that fails after its early `200` ends in an error frame without `mindshub`, so that failure reports no failover signal at all. **Turning failover off.** Send `X-MindsHub-Allow-Failover: false` to be served only by the model you asked for. Retries on that model still happen and are counted; if they all fail, you get its own error instead of a fallback's answer. Use it for evals, benchmarks, or a data-handling rule that allows one provider only. Any other value, or no header, keeps failover on. ### Reasoning effort Models with a non-null `reasoning_efforts` list accept an effort level; the levels vary by model, a level a model can't take is clamped onto its ladder rather than failing the request, and `reasoning_efforts: null` means the level isn't tunable, not that the model won't reason. Full rules and samples in [Reasoning](https://docs.mindshub.ai/inference/reasoning.md). ### Model behavior differences Wire behavior varies by provider: streaming chunk details, `tool_choice` handling, `finish_reason` mapping, and which sampling parameters are honored all differ. The per-model table is the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models); the per-feature notes are in each [guide](https://docs.mindshub.ai/inference/streaming.md). --- ## Prompt caching Prompt caching is automatic across the catalog: MindsHub marks the provider's cache breakpoints on the stable prefix of your request, so you get cache hits without doing anything. Wallet billing uses the provider's raw cached-token prices. On `mindshub_air`, cache reads and writes draw the included allowance at weights 1 and 12. ### Works with Every model whose provider caches: the Claude family (explicit breakpoints), the GPT and Grok families, Gemini, and Kimi (implicit). Per-model detail is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models). ### Request Nothing is required. On Messages you can place explicit `cache_control` breakpoints, as Claude Code does, and a request that sends its own breakpoints is passed through exactly as sent rather than re-marked. ```python Chat Completions # Caching is automatic. Keep the stable prefix (system prompt, tools) first. response = client.chat.completions.create( model="sonnet", messages=[ {"role": "system", "content": LONG_SYSTEM_PROMPT}, {"role": "user", "content": "Summarize section 3."}, ], ) print(response.usage.prompt_tokens_details) ``` ```python Responses # Caching is automatic. `instructions` is the stable prefix. response = client.responses.create( model="sonnet", instructions=LONG_SYSTEM_PROMPT, input="Summarize section 3.", ) print(response.usage.input_tokens_details) ``` ```python Messages message = client.messages.create( model="sonnet", max_tokens=1024, system=[{ "type": "text", "text": LONG_SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}, # explicit breakpoint }], messages=[{"role": "user", "content": "Summarize section 3."}], ) print(message.usage.cache_read_input_tokens, message.usage.cache_creation_input_tokens) ``` ```typescript Chat Completions // Caching is automatic. Keep the stable prefix (system prompt, tools) first. const response = await client.chat.completions.create({ model: "sonnet", messages: [ { role: "system", content: LONG_SYSTEM_PROMPT }, { role: "user", content: "Summarize section 3." }, ], }); console.log(response.usage?.prompt_tokens_details); ``` ```typescript Responses // Caching is automatic. `instructions` is the stable prefix. const response = await client.responses.create({ model: "sonnet", instructions: LONG_SYSTEM_PROMPT, input: "Summarize section 3.", }); console.log(response.usage?.input_tokens_details); ``` ```typescript Messages const message = await client.messages.create({ model: "sonnet", max_tokens: 1024, system: [{ type: "text", text: LONG_SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }], messages: [{ role: "user", content: "Summarize section 3." }], }); console.log(message.usage.cache_read_input_tokens, message.usage.cache_creation_input_tokens); ``` ### Response Cache activity shows up in `usage`: | API | Fields | |---|---| | Chat Completions | `usage.prompt_tokens_details: {"cached_tokens": …, "cache_write_tokens": …}`, present when the request touched the cache, reads **or** writes. `prompt_tokens` always includes cached tokens. `cache_write_tokens` is a MindsHub extension. | | Responses | `usage.input_tokens_details: {"cached_tokens": …, "cache_write_tokens": …}`. As in OpenAI's API, `input_tokens` always includes cached tokens. `cache_write_tokens` is a MindsHub extension. | | Messages | `usage.cache_read_input_tokens` and `usage.cache_creation_input_tokens`, always present. As in Anthropic's own API, `input_tokens` **excludes** cached tokens: the three input fields partition the prompt. | Don't treat the presence of `prompt_tokens_details` as "a cache read happened": writes alone populate it too. ### Per-model differences - **Claude family**: explicit `cache_control` breakpoints on Messages are honored; on the OpenAI-shaped APIs MindsHub places them for you. A request carrying its own breakpoints is never re-marked. - **GPT and Grok families**: the provider caches implicitly; MindsHub derives a stable `prompt_cache_key` from your instructions and tools so repeated prefixes hit. - **Gemini and Kimi**: implicit provider-side caching; reads are reported, writes are not billed separately. - **Fireworks-hosted models**: no cache accounting is reported today. Top-level `cache_control` (Anthropic's automatic-caching switch) on Messages is accepted and ignored; block-level breakpoints are what is honored. ### Errors you can hit None. Caching never fails a request. Cache writes are refused only as part of the request's overall funding check (`402 wallet_empty`). Prices are in [Billing](https://docs.mindshub.ai/inference/billing.md#what-gets-metered). --- ## Rate limits Two separate mechanisms can stop a request, and they return different errors: 1. **Throughput limits**: how fast you can go. Hitting one returns `429 rate_limited`; slowing down fixes it. 2. **Funding**: whether your organization's [included allowance or wallet](https://docs.mindshub.ai/inference/billing.md) can fund the request. Running out returns `429 included_allowance_exhausted` or `402 wallet_empty`, and a pause on free serving returns `429 free_air_daily_spend_fuse_exceeded`; slowing down does not fix these. ### Throughput limits Limits apply **per organization** (not per key, not per user), across all models combined: | Limit | Default | |---|---| | Requests per minute | 60 | | Tokens per minute | 1,000,000 | | Concurrent in-flight requests | 20 | These are the standard defaults; they can be raised per organization; contact [support@mindsdb.com](mailto:support@mindsdb.com) if your workload needs more. There is currently no API that reports your organization's limits or remaining headroom; the only signal is the `429` itself. Exceeding any of the three returns the same error: ``` HTTP 429 Retry-After: 12 {"error": {"message": "Rate limit exceeded for model 'sonnet'. Please slow down and retry.", "type": "rate_limit_error", "param": null, "code": "rate_limited"}} ``` Honor `Retry-After` (seconds, always ≥ 1). The response does not identify which of the three limits you exceeded; if you see 429s at low request rates, suspect the token budget or the concurrency cap. #### How the token budget is counted When a request is admitted, the per-minute token budget reserves the **larger of** your estimated prompt size and your `max_tokens`, then settles to the real count after the request finishes. Consequences: - A request with a huge `max_tokens` reserves that many tokens from the minute's budget up front, even if the model would have answered in 50. Oversized `max_tokens` values directly reduce how many requests you can run per minute. - A burst of concurrent large-`max_tokens` requests can exhaust the budget before any of them finishes. - A single request bigger than the whole per-minute budget isn't rejected outright; it waits for the budget to refill, surfacing as 429s until then. Keep `max_tokens` realistic, but not too tight on models that reason internally; see [the note in Chat completions](https://docs.mindshub.ai/inference/reasoning.md). #### No rate-limit headers on success Successful responses carry no `X-RateLimit-*` headers. Build client pacing on the 429s and `Retry-After`, not on header telemetry. ### Running out of tokens instead Distinct from throughput: your organization's recurring **included allowance** can run out, the wallet can run dry, and free serving can be paused for everyone. Retrying does not help with any of them: - `429 included_allowance_exhausted`: your own allowance is used up. Wait for `reset_at` in the body or `X-MindsHub-Reset-At` in the headers, or add credit. When neither is present, your organization's included allowance is zero (never granted, or set to zero), so waiting will not help and only credit will. - `429 free_air_daily_spend_fuse_exceeded`: free serving is paused for everyone, even when your own allowance is untouched. The same `reset_at` and `X-MindsHub-Reset-At` say when free serving resumes. Wait until then, or add credit to continue immediately. - `402 wallet_empty`: the request needs wallet credit. Add credit, then send it again. Neither billing `429` sends `Retry-After`. Both send `x-should-retry: false`, so the OpenAI and Anthropic SDKs hand them to you on the first attempt instead of retrying. [Billing](https://docs.mindshub.ai/inference/billing.md) shows how to read the remaining percentage; [Errors](https://docs.mindshub.ai/inference/errors.md) has the exact shapes. ### Practical guidance - Cap client-side concurrency at or below 20, and queue beyond it. - Retry `429 rate_limited` honoring `Retry-After`. Stop on `included_allowance_exhausted` and `free_air_daily_spend_fuse_exceeded`, and on any response that carries `x-should-retry: false`. A worked loop is in [Errors](https://docs.mindshub.ai/inference/errors.md#a-retry-recipe). - Don't rely on the limiter as flow control: under degraded operation limits may not be enforced exactly, and nothing guarantees the 429 arrives precisely at the documented threshold. --- ## Reasoning Many current models reason before answering. Where the depth is adjustable, it is a parameter: `reasoning_effort` on Chat Completions, `reasoning.effort` on Responses, `output_config.effort` (or a `thinking` object) on Messages. Where it isn't adjustable, the model still reasons, and you pay for it as output tokens. ### Works with Models whose [catalog entry](https://docs.mindshub.ai/inference/models.md) lists `reasoning_efforts`. The levels vary by model and `GET /v1/models` is authoritative; the per-model ladder is also on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models). Models with `reasoning_efforts: null` (`mindshub_air`, `kimi` among them) reason internally on every request and the level isn't tunable. A model can also **pin** the level it runs at without offering a choice of it: a null `reasoning_efforts` alongside a non-null `default_reasoning_effort`. `mindshub_air` is pinned to `medium` this way, so every request reasons at `medium` and a `reasoning_effort` you send is replaced by the pinned level rather than honored — reported as a clamp in `X-MindsHub-Clamped-Params`. Pick a model that publishes a ladder if you need to choose the level. ### Request ```python Chat Completions response = client.chat.completions.create( model="deepseek-v4-flash", messages=[{"role": "user", "content": "Prove that √2 is irrational."}], reasoning_effort="high", ) print(response.choices[0].message.content) ``` ```python Responses response = client.responses.create( model="deepseek-v4-flash", input="Prove that √2 is irrational.", reasoning={"effort": "high"}, ) print(response.output_text) ``` ```python Messages message = client.messages.create( model="deepseek-v4-flash", max_tokens=4096, messages=[{"role": "user", "content": "Prove that √2 is irrational."}], output_config={"effort": "high"}, ) print(message.content[-1].text) ``` ```typescript Chat Completions const response = await client.chat.completions.create({ model: "deepseek-v4-flash", messages: [{ role: "user", content: "Prove that √2 is irrational." }], reasoning_effort: "high", }); console.log(response.choices[0].message.content); ``` ```typescript Responses const response = await client.responses.create({ model: "deepseek-v4-flash", input: "Prove that √2 is irrational.", reasoning: { effort: "high" }, }); console.log(response.output_text); ``` ```typescript Messages const message = await client.messages.create({ model: "deepseek-v4-flash", max_tokens: 4096, messages: [{ role: "user", content: "Prove that √2 is irrational." }], output_config: { effort: "high" }, }); console.log(message.content.at(-1)); ``` These samples use `deepseek-v4-flash`, which takes `none`, `low`, `high` and `max` and draws the prepaid wallet. **Requests never fail over the effort level.** 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`. An unrecognized value such as a typo fits no rung. On a model that publishes a `default_reasoning_effort` you get that level, clamped onto the ladder like any other, and it is reported as a clamp; only a model that publishes no default drops the parameter and reports it in `X-MindsHub-Dropped-Params`, letting the provider's own default apply. On models with `reasoning_efforts: null` and no default the drop still comes with no header. **A clamp on an unrecognized level names the category `other`, not the value you sent** — `reasoning_effort=other>medium`, never `reasoning_effort=hgih>medium` — because a level we don't recognize is caller-supplied text and isn't echoed back in a response header. A level we do recognize is named exactly as you sent it. One consequence worth planning for: a typo no longer lands on the provider's default, it lands on the model's published one. On models whose default is the top of their ladder (`glm`, `qwen`, `gemini`, `grok-4-5` among them) that is the dearest rung; on `gpt-mini` and `gpt-nano`, whose default is `none`, it turns reasoning off. Read `default_reasoning_effort` off `GET /v1/models` if that matters to you, and send a level you have validated. 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. #### Extended thinking on Messages A `thinking` object on a Messages request (`{"type": "enabled", "budget_tokens": 8000}`) is forwarded to the target model where that model accepts it, including its `budget_tokens`; the current Claude 5 models accept it. `thinking` and `redacted_thinking` blocks in your message history are replayed to the model unmodified, so a multi-turn tool workflow on a thinking model keeps working. On the OpenAI-shaped APIs there is no `thinking` object; use the effort level. #### Carrying reasoning across turns on Responses `include: ["reasoning.encrypted_content"]` carries a reasoning model's thinking across chained turns (`previous_response_id`); the other `include` values are ignored. `reasoning.summary` asks for a summary on the returned `reasoning` item. ### Response - **Chat Completions and Responses never return reasoning content**; you get the final answer. `usage.completion_tokens_details.reasoning_tokens` (or `output_tokens_details.reasoning_tokens`) is a **subset** of the output count, not an addition to it, 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. - **Messages returns `thinking` blocks**, on streamed and non-streamed turns alike, in Anthropic's required order (before the text), with their `signature`, usually with empty or withheld content. Don't assume the first block is your text. ### Streaming On Messages, thinking blocks stream as `content_block_*` events before the text and carry a `signature` so a streaming client can replay them. On the OpenAI-shaped APIs nothing about the stream changes. See [Streaming](https://docs.mindshub.ai/inference/streaming.md). ### Per-model differences - Levels differ by model: `sonnet` supports `low` through `max`, `deepseek-v4-flash` takes `none`, `low`, `high` and `max`, `gpt-mini` adds `none`. Read the model's own list rather than assuming a family shares one ladder. - `mindshub_air` and `kimi` reason internally with no adjustable level. 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"`. - `mindshub_air` is pinned to `medium`: it publishes no ladder but does publish a `default_reasoning_effort`, so every request reasons at that level and one you send is replaced by it. `kimi` publishes neither, so a level you send it is dropped with no header. - Gemini sets depth through `thinking_level`, which the effort parameter carries; it accepts no `thinking` object. ### Errors you can hit None specific to this feature: an out-of-ladder level is clamped, and an unrecognized one falls back to the model's published default where it has one and is dropped where it does not — all reported in headers. A turn whose reasoning exhausted `max_tokens` ends with `finish_reason: "length"` (`stop_reason: "max_tokens"` on Messages, `status: "incomplete"` on Responses). See [Errors](https://docs.mindshub.ai/inference/errors.md). --- ## Responses(2) `POST /v1/responses` implements the OpenAI Responses API. The OpenAI SDKs work against it with a `base_url` change, and **any model in the [catalog](https://docs.mindshub.ai/inference/models.md) can serve a Responses request**, not just OpenAI's. This is also the format [OpenAI Codex](https://docs.mindshub.ai/inference/coding-agents.md#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 work on any model, and turns can be chained by `previous_response_id` instead of resending the conversation. One gap worth knowing before you start: **OpenAI's separate Conversations API (the `conversation` parameter) is not supported**; it is rejected with a `400`. Chain turns with `previous_response_id` instead. ### Endpoint `POST https://api.mindshub.ai/v1/responses`, plus the stored-response operations `GET`/`DELETE /v1/responses/{id}`, `POST /v1/responses/{id}/cancel`, and `GET /v1/responses/{id}/input_items`. OpenAI SDKs take the base URL **with** `/v1` and authenticate with `api_key`. ### Quick example ```python Python 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="mindshub_air", input="What is the capital of Australia?", ) print(response.output_text) ``` ```typescript TypeScript 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: "mindshub_air", input: "What is the capital of Australia?", }); console.log(response.output_text); ``` ### Request parameters | Parameter | Handling | Notes | |---|---|---| | `model` | adapted | An alias from GET /v1/models resolves to a concrete provider model, so any catalog model can serve a Responses request. The response names the model that served, as every lane does; `mindshub_air` and `mindshub_blaze` return their own alias instead. | | `input` | adapted | A string or an item list. `message`, `function_call`, `function_call_output`, `custom_tool_call`, `custom_tool_call_output` and `reasoning` items round-trip; `input_text`/`output_text`/`input_image` content parts translate, including inside a `function_call_output`, so a tool that returns an image shows the model an image; a model that cannot take one sees a short text note in its place. Supplied `refusal` explanations replay as text. `input_file` and `input_audio` parts are dropped (Milestone 4). An `item_reference` item has no local counterpart and is skipped. Every skipped item type (`item_reference`, `local_shell_call`, …) is named in X-MindsHub-Dropped-Params as `input.`. | | `instructions` | adapted | Becomes a leading system message. Deliberately per-turn: OpenAI does not carry it across `previous_response_id`, so a chained turn replays history without it. | | `stream` | forwarded | SSE Responses events terminated by `response.completed`/`.incomplete`, never a `[DONE]` sentinel. Items are strictly sequential — one closes before the next opens. | | `stream_options` | ignored | The Responses API's only member is `include_obfuscation`, which we do not implement; usage rides the terminal event unconditionally, so there is nothing here to honour. `include_usage` is read as a MindsHub extension for callers migrating from the chat lane, and changes nothing a Responses client can observe. | | `max_output_tokens` | conditional | Clamped down to the resolved model's output ceiling and up to the transport's floor; either is named in X-MindsHub-Clamped-Params. A turn cut short by it reports `status: incomplete` with `incomplete_details.reason: max_output_tokens`. | | `temperature` | conditional | Dropped on models that reject sampling params and named in X-MindsHub-Dropped-Params. One body routes to seven providers, so a param the chosen model cannot read is not a reason to refuse the request. | | `top_p` | conditional | As temperature. On a Claude model that still takes sampling params, sending it with temperature drops top_p (Anthropic rejects the pair) and names it in X-MindsHub-Dropped-Params; top_p alone is forwarded. | | `verbosity` | conditional | Not in the SDK's request-params object. `responses.parse()` takes it as a top-level keyword and passes it through verbatim, so it arrives beside `text` rather than inside it; `responses.create()` does not model it at all, so `parse()` is the only SDK route to this parameter. Normalized into `text.verbosity` on arrival, so the echo and the applied value agree, and honoured from there. | | `reasoning` | conditional | `effort` maps onto each model's own ladder, clamped rather than dropped so a request to be cheap is not answered by the provider's near-the-top default. `summary` is surfaced on the returned `reasoning` item where the provider gives one. Kimi reasons internally with no adjustable level and declares `effort` unsupported. | | `include` | conditional | Of the eight values OpenAI defines, only `reasoning.encrypted_content` is honoured — it gates the reasoning round-trip. Note that `web_search_call` items and `output_text` annotations are returned whether or not they are asked for, so the two `include` values naming them change nothing. `message.output_text.logprobs` needs logprobs we do not produce; the file_search, code_interpreter and computer_use values belong to hosted tools we do not run; `message.input_image.image_url` is Milestone 4. | | `tools` | adapted | Responses function tools are flat where the internal shape nests them; both translate. Hosted `web_search` maps onto each provider's own search or our external loop, and the searches it runs are reported as `web_search_call` output items on a non-streamed turn (a stream does not narrate them yet). `mcp` runs on the target's own remote-MCP connector (GPT: OpenAI's, `require_approval` passed through, approvals included; Claude: Anthropic's, which has no gate, so only tools resolving to `never` run and the rest are named in X-MindsHub-Dropped-Params) and is reported as `mcp_list_tools` / `mcp_call` output items on a non-streamed turn. A freeform `custom` tool (Codex's `apply_patch`) goes upstream as a function taking one string, its grammar in the description, and the call comes back as a `custom_tool_call`. Every other hosted tool (file_search, code_interpreter, computer_use, local_shell, tool_search, …) has no cross-provider meaning, so it is dropped and named in X-MindsHub-Dropped-Params as `tools.`. | | `tool_choice` | adapted | auto/required/none and `{type: function, name}` all map. A forced choice a model cannot honour is downgraded to `auto` rather than refused (ENG-1095). A hosted-tool choice cannot be forced generically and is dropped. | | `parallel_tool_calls` | conditional | Honoured, including on the Anthropic wire, which spells it inverted inside tool_choice. Gemini cannot express it and declares it unsupported, so the drop is named in X-MindsHub-Dropped-Params. | | `text` | conditional | `text.format` is structured output and is honoured on every transport that can express a JSON schema; a model that cannot is a 400 naming it, not a silent drop, because prose handed to a caller about to json.loads() it is a different kind of answer. Schema-less `json_object` is unavailable on the Anthropic family, which has no equivalent. `text.verbosity` reaches only the OpenAI Responses transport, whose native field it is. | | `store` | adapted | Defaults to true as OpenAI's server does. A stored turn is readable by GET, DELETE and /input_items, and chainable by previous_response_id, for 30 days. `store: false` persists nothing — a chain can still be read from, but not extended. | | `previous_response_id` | adapted | Reconstructs history from the stored chain. Items the client also re-sent are deduped, because a duplicated tool_use is a hard 400 on the Anthropic wire. An unknown id is a 404 naming the recovery. A chain truncated to fit is reported in a response header. | | `background` | ignored | Accepted and echoed, but every request is served synchronously: a client's poll of retrieve() finds the response already completed rather than queued. `response.queued` is therefore never emitted, and cancel() has nothing in flight to stop. | | `metadata` | ignored | Accepted and echoed back on the Response. Describes the caller's bookkeeping rather than the generation, so it reaches no provider and is not reported as a drop. | | `user` | ignored | As safety_identifier — OpenAI's deprecated spelling of it. | | `safety_identifier` | ignored | Accepted and echoed. Describes the caller, not the generation. Abuse attribution here runs off the authenticated identity, which a request cannot choose. | | `service_tier` | ignored | Accepted and echoed as `default`. One serving tier here, so the field is a constant rather than a measurement — sent because OpenAI always sends it and a client reading it should get a string rather than a missing key. | | `prompt_cache_key` | ignored | Accepted and echoed. We derive an equivalent key ourselves from the instructions and tool set, so honouring the caller's spelling would change nothing they can observe. | | `prompt_cache_retention` | ignored | Accepted and echoed. Cache lifetime is the upstream provider's to set and differs per provider; there is no field to forward it to on any transport we speak. | | `top_logprobs` | ignored | Our OpenAI and xAI providers speak the Responses API, which has no logprobs, and the Anthropic wire has none either — so there is no response-side plumbing to build on for the two backends that could express it. Named in X-MindsHub-Dropped-Params, and `logprobs: []` is sent on every text frame because the SDK requires the key. | | `truncation` | ignored | `disabled` IS our behavior and is honoured by doing nothing. `auto` asks us to drop history to fit the context window; we have no per-model context table, so it is named in X-MindsHub-Dropped-Params rather than silently not happening. | | `max_tool_calls` | ignored | No transport we speak has a per-turn tool-call ceiling, and enforcing one ourselves would mean truncating a turn mid-flight. Named in X-MindsHub-Dropped-Params. | | `conversation` | rejected (400) | A 400. OpenAI documents `conversation` and `previous_response_id` as mutually exclusive, so the alternative is a one-line change for the caller. Refusing beats ignoring here: a client that believes we are holding its thread stops sending history, and every turn silently loses its context (ENG-1224). No third-party Responses implementation offers a Conversations API today. | | `prompt` | rejected (400) | A 400. Stored prompt templates live in OpenAI's dashboard and we cannot resolve an id that only exists there; serving the request would answer with the template silently missing. | | `context_management` | rejected (400) | A 400. It asks us to manage the context window on the caller's behalf, and a client that believes we are doing so stops managing it itself — the same failure as `conversation`. | *This table is generated from `PARAM_SUPPORT` in `minds/requests/responses_compat.py`, which a unit test holds to the vendor SDK's own parameter list. The cross-API view is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#request-parameters-by-api).* `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. The cross-API view of every parameter is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#request-parameters-by-api). #### Input `input` takes a plain string, or an array of items for multi-turn conversations and richer content: ```json { "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 [Tool calling](https://docs.mindshub.ai/inference/tool-calling.md). Roles are `system`, `user`, and `assistant`. Prefer `instructions` over a `system` item for system-level guidance; both work. ### Request body The full request and response schemas, with every field described, are on the generated reference pages: [`POST /v1/responses`](/api-reference/create-response), [`GET /v1/responses/{id}`](/api-reference/get-response), [`DELETE /v1/responses/{id}`](/api-reference/delete-response), [`cancel`](/api-reference/cancel-response), and [`input_items`](/api-reference/list-response-input-items). ### Response ```json { "id": "resp_9f2c1ae0b4d8", "object": "response", "created_at": 1785401283, "status": "completed", "model": "claude-sonnet-5", "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_text` is the flattened assistant text, the same convenience field the OpenAI SDK exposes. `output` carries the structured items: `message` items, `function_call` items when the model calls tools, `web_search_call` items when it searched, `mcp_list_tools` / `mcp_call` / `mcp_approval_request` items when it used a remote MCP server, and a `reasoning` item on reasoning models. - `model` names the model that served, as on [Chat Completions](https://docs.mindshub.ai/inference/chat-completions.md); `mindshub_air` and `mindshub_blaze` return their own alias. See [Models](https://docs.mindshub.ai/inference/models.md#what-the-responses-model-field-contains). - `input_tokens_details.cached_tokens` reports prompt cache reads. See [Prompt caching](https://docs.mindshub.ai/inference/prompt-caching.md). - `output_tokens_details.reasoning_tokens` is a subset of `output_tokens`, broken out where the provider reports it. See [Reasoning](https://docs.mindshub.ai/inference/reasoning.md). - `status` is `completed`, `incomplete` (with `incomplete_details.reason` set to `max_output_tokens` or `content_filter`), or `failed`. - When the provider explains a refusal, the response includes a `{"type": "refusal", "refusal": "..."}` content part carrying that explanation, in place of (or beside) the `output_text` part. No refusal text is invented when the provider supplies none. `output` is never empty: a turn that produced nothing still returns a `message` item with empty text, so "declined" and "said nothing" stay distinguishable. - `output_text.annotations` carries the model's web-search sources as `url_citation` entries. See [Web search](https://docs.mindshub.ai/inference/web-search.md). ### Streaming events `"stream": true` produces the typed Responses event stream (`response.created` … `response.completed`), 15 of the protocol's 53 event types. The sequence, the terminal `response.incomplete`/`response.failed` events, and the list of events this API never emits are in [Streaming](https://docs.mindshub.ai/inference/streaming.md#responses). ### Lane-specific behaviour - **Server-side state.** Turns are stored for 30 days by default and chain by `previous_response_id`; `GET`, `DELETE`, `input_items`, and `cancel` operate on stored turns. The boundaries (25-turn chain walk, `X-MindsHub-Chain-Truncated`, expiry) are in [Conversation state](https://docs.mindshub.ai/inference/conversation-state.md#server-side-state-on-responses). - **`background: true` is accepted but every request completes synchronously**; `cancel` on anything else returns `400`. - **Trailing slash.** `POST /v1/responses/` is served identically to `POST /v1/responses`, so a client that appends the slash is not redirected with a body-dropping 307. - **Codex** speaks this format exclusively, so `wire_api = "responses"` runs it on any catalog model. See [Codex](https://docs.mindshub.ai/inference/codex.md). ### Errors Errors use the OpenAI envelope, so SDK error handling works unmodified: ```json { "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" } } ``` A rejected `conversation`, `prompt`, or `context_management` field is `400 unsupported_parameter`; an unknown `previous_response_id` is `404 previous_response_not_found`. Status codes and their meanings are shared across the API; see [Errors](https://docs.mindshub.ai/inference/errors.md). *The per-parameter contract this page summarizes is maintained in code, as `PARAM_SUPPORT` in `minds/requests/responses_compat.py`, and held to the OpenAI SDK's own parameter list by a test.* --- ## Streaming Set `stream` and the response arrives as a `text/event-stream` instead of one JSON body. Each API streams in its own native format, so the streaming code you already have keeps working: Chat Completions sends `chat.completion.chunk` frames, Responses sends typed `response.*` events, Messages sends Anthropic's `message_start` … `message_stop` sequence. ### Works with Every generation model, on all three APIs. Per-model differences in tool-call streaming and usage frames are listed [below](#per-model-differences) and in the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#features-by-api). ### Request ```python Chat Completions stream = client.chat.completions.create( model="mindshub_air", messages=[{"role": "user", "content": "Count to five."}], stream=True, stream_options={"include_usage": True}, ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) ``` ```python Responses stream = client.responses.create( model="mindshub_air", input="Count to five.", stream=True, ) for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="", flush=True) ``` ```python Messages with client.messages.stream( model="mindshub_air", max_tokens=1024, messages=[{"role": "user", "content": "Count to five."}], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) ``` ```typescript Chat Completions const stream = await client.chat.completions.create({ model: "mindshub_air", messages: [{ role: "user", content: "Count to five." }], stream: true, stream_options: { include_usage: true }, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) process.stdout.write(delta); } ``` ```typescript Responses const stream = await client.responses.create({ model: "mindshub_air", input: "Count to five.", stream: true, }); for await (const event of stream) { if (event.type === "response.output_text.delta") process.stdout.write(event.delta); } ``` ```typescript Messages const stream = client.messages.stream({ model: "mindshub_air", max_tokens: 1024, messages: [{ role: "user", content: "Count to five." }], }); for await (const text of stream.textStream) process.stdout.write(text); ``` The samples assume a `client` built as shown in [Getting started](https://docs.mindshub.ai/inference/getting-started.md#2-the-same-request-three-ways). Guard on `chunk.choices` being non-empty in the Chat Completions loop: the stream ends with a usage-bearing chunk that carries no choices. ### Long-running requests Stream any request that can take longer than about a minute and a half. A non-streaming request gets 110 seconds in total, counted from when it reaches the API, failover attempts and web-search turns included, and then returns our own [`assembly_timeout`](https://docs.mindshub.ai/inference/errors.md#error-catalog) error. A stream is not held to that limit. The limit comes from the edge in front of the API, which closes a connection that sends nothing for 125 seconds. A stream keeps the connection busy in two ways: - **While the model thinks, the stream sends keepalives.** A reasoning model can go minutes without writing anything. After 15 seconds without a frame, the stream sends an SSE comment line, `: keepalive`, and repeats it until output resumes. The SSE format tells every parser to skip lines that start with `:`, and the OpenAI and Anthropic SDKs do. A hand-written parser has to skip them too. A client that times out when no event arrives, rather than no bytes, can still close a stream while the model is quiet: Codex does after five minutes unless you raise its timeout (see [Codex](https://docs.mindshub.ai/inference/codex.md#configure-codex)). - **A slow start gets its `200` early.** If the response hasn't started after 90 seconds, the `200` and SSE headers go out anyway, followed by keepalives until it does. This happens when the upstream is slow to answer, or while the server-side search loop runs on Fireworks-hosted models and Kimi. After that point a failure arrives inside the stream, as described for each API below, and the response carries none of the [`X-MindsHub-*` headers](https://docs.mindshub.ai/inference/errors.md#the-x-mindshub--headers). Your SDK does not retry a failure that arrives inside a stream, so retry it yourself where you would have retried the same failure's status (see [Streaming failures](https://docs.mindshub.ai/inference/errors.md#streaming-failures)). A stream can still end early in two ways, and both cut it off without its final event, so treat a stream that ends that way as failed (see [Streaming failures](https://docs.mindshub.ai/inference/errors.md#streaming-failures)). Once its response has started, a stream that sends nothing but keepalives for 30 minutes is ended, whether the model is still thinking or its provider has stalled. And a stream that runs longer than ten minutes can be cut if the server holding it restarts, for example during a deploy. ### Response #### Chat Completions A series of `data: ` lines, each carrying a `chat.completion.chunk`, terminated by `data: [DONE]`: ``` data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"","role":"assistant"},"index":0}],"created":1785401212,"model":"haiku","object":"chat.completion.chunk"} data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"Hello"},"index":0}],"created":1785401212,"model":"haiku","object":"chat.completion.chunk"} data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop","index":0}],"created":1785401212,"model":"haiku","object":"chat.completion.chunk"} data: [DONE] ``` 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. `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. - **Errors before the stream opens 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`. The exception is a request whose response took more than 90 seconds to start: its `200` has already gone out, so the failure arrives as a `data:` frame whose object has an `error` key, and the OpenAI SDKs raise `APIError` on it. - **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](https://docs.mindshub.ai/inference/errors.md#streaming-failures). #### Responses Typed events, 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 ``` 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. A call to a freeform `custom` tool arrives as a `custom_tool_call` item instead: its text comes in one `response.custom_tool_call_input.delta`, then `response.custom_tool_call_input.done`, once the call is complete. Provider refusal explanations stream as `response.refusal.delta` events followed by `response.refusal.done`, inside a `refusal` content part. A refused turn terminates with `response.incomplete` and `incomplete_details.reason: "content_filter"`. When the provider supplies no explanation, no refusal text events are emitted; the terminal reason still identifies the refusal. 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 response `Content-Type` before parsing. If the response took more than 90 seconds to start, the `200` has already gone out, and the failure arrives as `response.created` followed by `response.failed`. That event's `response.error.code` is the string `code` the same failure has as a JSON error, so it can fall outside the five listed below: one of ours, such as `param_not_supported`, or the upstream provider's own. A failure whose JSON error has no string `code` gets one of the five instead. - **A mid-stream failure is reported in-band as a `response.failed` event**, because the HTTP status is already `200` by then. It carries `response.error.code` (`rate_limit_exceeded`, `context_length_exceeded`, `insufficient_quota`, `invalid_prompt` or `server_error`) plus whatever output was produced before the failure, marked `incomplete`. `response.failed` and `response.completed` are mutually exclusive. - **A truncated turn terminates with `response.incomplete`**, not `response.completed`, with `incomplete_details.reason` set to `max_output_tokens` or `content_filter`. Two of those `error.code` values, `context_length_exceeded` and `insufficient_quota`, are ours rather than OpenAI's, which defines neither. They are there because they are actionable. Clients that switch on the OpenAI enum should treat an unrecognized code as a generic retryable error, which is what Codex does. Note that the SDK's `stream.get_final_response()` raises on anything but `response.completed`, so read the terminal event directly if you need to handle a truncated turn. ##### Events we don't emit The Responses protocol defines 53 event types; this API emits 15, including the refusal events described above, `response.incomplete`, and `response.failed`. The remaining events are not emitted, so a client waiting on one of them waits forever. The absences, and why: | Events | Why | |---|---| | `response.audio.*`, `response.image_gen_call.*` | Audio and image output aren't supported yet. | | `response.file_search_call.*`, `response.code_interpreter_call.*` | Hosted tools this API doesn't run. Those tool entries are dropped from `tools` and named in `X-MindsHub-Dropped-Params`. | | `response.mcp_*` | [Remote MCP](https://docs.mindshub.ai/inference/tool-calling.md#remote-mcp-servers) runs, but a stream does not narrate it yet: a non-streamed turn reports `mcp_list_tools` / `mcp_call` items in `output`; a streamed one emits no `mcp_*` events, so a streaming client has nothing to replay and the server's tool list is fetched again next turn. | | `response.queued` | Nothing is ever queued; `background` requests are served synchronously. | | `response.web_search_call.*` | A non-streamed turn reports its searches as `web_search_call` items in `output`. The **streaming** events are not emitted yet, so a streamed search is still silent. | | `response.reasoning_summary_*`, `response.reasoning_text.*` | Reasoning is delivered as a single `reasoning` item at the end of the stream, not as deltas. | | `response.output_text.annotation.added` | Citations arrive on the finished `output_text` part, not as incremental events. On a stream they land on the terminal `response.completed` event. | #### Messages The standard Anthropic event sequence: ``` event: message_start → event: ping → event: content_block_start → event: content_block_delta … → event: content_block_stop → event: message_delta → event: message_stop ``` Differences from Anthropic's native stream: - **`ping` is sent once**, right after `message_start`, not periodically. Don't use pings as a liveness signal. What keeps a silent stream open is the `: keepalive` comment line described in [Long-running requests](#long-running-requests), which the SDKs skip. - **On non-Claude models, `message_start` reports `input_tokens: 0`.** The real counts arrive in the final `message_delta`. Token-tracking UIs will undercount mid-stream on non-Claude models. - **A failure after the body opens arrives as an `error` event**, typed exactly as the same failure would have been before the body opened, and the SDKs raise it. A mid-generation failure can still surface as a normal-looking completion with truncated or empty content; if your application must detect failed generations, sanity-check the output as well. - **Thinking blocks stream too**, in Anthropic's required order, with their `signature`, so a streaming client can replay them on its next request. See [Reasoning](https://docs.mindshub.ai/inference/reasoning.md). ### Per-model differences - **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. - **Web search on Fireworks-hosted models and Kimi is not streamed live.** Those models search through a server-side loop; the finished answer is replayed to you as a stream once the loop completes, so the first token arrives late. A loop that runs past 90 seconds gets its `200` early, with keepalives until the answer arrives. - **Gemini models always send a terminal frame**, even on failure, so the "no `finish_reason` means failure" rule above does not apply to them. ### Errors you can hit A request rejected before generation (`400`, `401`, `402`, `404`, `429`) is a plain JSON error in the lane's envelope, not an SSE stream. The exception is a response that took more than 90 seconds to start: its rejection arrives inside the stream, as described in [Long-running requests](#long-running-requests). Mid-stream failures are lane-specific, as described above. The full catalog is in [Errors](https://docs.mindshub.ai/inference/errors.md). --- ## Structured output Ask for a JSON schema and the model's answer is JSON matching it. Every API has its own spelling for the request, and all three are honored: `response_format` on Chat Completions, `text.format` on Responses, `output_config.format` on Messages. ### Works with Every model that can constrain its output. This is **the one parameter that is refused rather than dropped**: a model that cannot constrain its output returns `400 param_not_supported`, because prose handed to a caller who is about to `json.loads()` it fails later and less legibly than an error here. The per-model column is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models). ### Request ```python Chat Completions from pydantic import BaseModel class City(BaseModel): name: str country: str population: int completion = client.chat.completions.parse( model="mindshub_air", messages=[{"role": "user", "content": "Describe Lisbon."}], response_format=City, ) city = completion.choices[0].message.parsed print(city.population) ``` ```python Responses from pydantic import BaseModel class City(BaseModel): name: str country: str population: int response = client.responses.parse( model="mindshub_air", input="Describe Lisbon.", text_format=City, ) print(response.output_parsed.population) ``` ```python Messages message = client.messages.create( model="mindshub_air", max_tokens=1024, messages=[{"role": "user", "content": "Describe Lisbon."}], output_config={ "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "name": {"type": "string"}, "country": {"type": "string"}, "population": {"type": "integer"}, }, "required": ["name", "country", "population"], "additionalProperties": False, }, } }, ) import json city = json.loads(message.content[0].text) print(city["population"]) ``` ```typescript Chat Completions import { zodResponseFormat } from "openai/helpers/zod"; import { z } from "zod"; const City = z.object({ name: z.string(), country: z.string(), population: z.number() }); const completion = await client.chat.completions.parse({ model: "mindshub_air", messages: [{ role: "user", content: "Describe Lisbon." }], response_format: zodResponseFormat(City, "city"), }); console.log(completion.choices[0].message.parsed?.population); ``` ```typescript Responses import { zodTextFormat } from "openai/helpers/zod"; import { z } from "zod"; const City = z.object({ name: z.string(), country: z.string(), population: z.number() }); const response = await client.responses.parse({ model: "mindshub_air", input: "Describe Lisbon.", text: { format: zodTextFormat(City, "city") }, }); console.log(response.output_parsed?.population); ``` ```typescript Messages const message = await client.messages.create({ model: "mindshub_air", max_tokens: 1024, messages: [{ role: "user", content: "Describe Lisbon." }], output_config: { format: { type: "json_schema", schema: { type: "object", properties: { name: { type: "string" }, country: { type: "string" }, population: { type: "integer" }, }, required: ["name", "country", "population"], additionalProperties: false, }, }, }, }); const city = JSON.parse((message.content[0] as { text: string }).text); console.log(city.population); ``` The raw wire spellings, if you are not using an SDK helper: | API | Field | |---|---| | Chat Completions | `response_format: {"type": "json_schema", "json_schema": {"name": …, "schema": …, "strict": true}}` | | Responses | `text: {"format": {"type": "json_schema", "name": …, "schema": …, "strict": true}}` | | Messages | `output_config: {"format": {"type": "json_schema", "schema": …}}` | Schemas are forwarded to the target model as you wrote them; nothing is silently rewritten to make a schema fit. Where a provider's schema dialect is narrower than JSON Schema, the request is adapted at that provider's edge, and a schema the provider rejects returns that provider's own 400 naming the keyword at fault. ### Response The answer arrives as the API's normal text: `message.content` on Chat Completions, `output_text` on Responses, a `text` block on Messages. It parses as JSON matching your schema. Responses echoes the `text.format` you sent back on the response object, and `responses.parse()` / `chat.completions.parse()` populate their `parsed` fields. ### Streaming Structured output streams like any other text; the JSON arrives in fragments and is complete when the turn ends. Parse after the final frame. A schema request that a model can't honor fails before the stream opens, as a plain `400`. ### Schema-less JSON mode `{"type": "json_object"}` (Chat Completions and Responses) asks for valid JSON without a schema. It is honored on every transport except the Claude family, where it is a `400`: Anthropic's API has no schema-less mode and no faithful way to fake one (its schemas mandate `additionalProperties: false`, so the obvious stand-in constrains the answer to the literal empty object). Send a schema instead. Messages has no spelling for JSON mode at all. ### Per-model differences - **Claude family** (`opus`, `sonnet`, `fable`, `haiku`): schema output yes, JSON mode `400`. - **`muse-spark` and its pins** (Meta): whether Meta's endpoint actually enforces the schema when sent natively is unverified, so `response_format`/`text.format` is served by emulation instead — offered as a synthetic tool the model can call, and unwound back into a normal answer before it reaches you, so the response shape looks the same as everywhere else. Because this alias's `tool_choice` only ever accepts `"auto"`, the call can't be forced: a well-behaved model calls it and you get schema-conformant JSON, but a model that answers directly instead is served as-is rather than refused. Best-effort, not guaranteed, for this one alias. - **Gemini models**: a schema and `tools` in the same request is a provider-side conflict and fails upstream. - **Fireworks-hosted models** (`deepseek-v4-flash`, `qwen`, `glm`, `kimi`): both schema and JSON mode work. The full per-model column is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models). ### Errors you can hit `400 param_not_supported` when the model cannot constrain its output. The error's `param` names the field in your own API's spelling (`response_format`, `text.format`, or `output_config.format`). A schema the provider rejects returns the provider's own `400`. See [Errors](https://docs.mindshub.ai/inference/errors.md). --- ## Tool calling Declare tools, and when the model wants one the response carries a call with JSON arguments. You run the function and send the result back as the next turn. The round trip works on every model, in each API's own shape: OpenAI's `tools`/`tool_calls` on Chat Completions, the flat `function_call` items on Responses, `tool_use`/`tool_result` blocks on Messages. ### Works with Every generation model. What differs per model is how far a *forced* `tool_choice` is honored and whether `"none"` has a native spelling; see [Per-model differences](#per-model-differences) and the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models). ### Request Declare a function, call the model, run the tool, and send the result back with the same `tools` array. ```python Chat Completions tools = [{ "type": "function", "function": { "name": "get_weather", "description": "Get the current weather for a city", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }, }] messages = [{"role": "user", "content": "What's the weather in Lisbon?"}] response = client.chat.completions.create(model="mindshub_air", messages=messages, tools=tools) reply = response.choices[0].message if not reply.tool_calls: raise RuntimeError(f"Expected a tool call, got prose: {reply.content}") call = reply.tool_calls[0] result = "19°C, light rain" # run the real function here messages.append(reply) messages.append({"role": "tool", "tool_call_id": call.id, "content": result}) final = client.chat.completions.create(model="mindshub_air", messages=messages, tools=tools) print(final.choices[0].message.content) ``` ```python Responses tools = [{ "type": "function", "name": "get_weather", "description": "Get the current weather for a city", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }] first = client.responses.create(model="mindshub_air", input="What's the weather in Lisbon?", tools=tools) calls = [item for item in first.output if item.type == "function_call"] if not calls: raise RuntimeError(f"Expected a tool call, got prose: {first.output_text}") call = calls[0] result = "19°C, light rain" # run the real function here final = client.responses.create( model="mindshub_air", previous_response_id=first.id, input=[{"type": "function_call_output", "call_id": call.call_id, "output": result}], tools=tools, ) print(final.output_text) ``` ```python Messages tools = [{ "name": "get_weather", "description": "Get the current weather for a city", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }] messages = [{"role": "user", "content": "What's the weather in Lisbon?"}] first = client.messages.create(model="mindshub_air", max_tokens=1024, messages=messages, tools=tools) uses = [block for block in first.content if block.type == "tool_use"] if not uses: raise RuntimeError("Expected a tool call, got prose") use = uses[0] result = "19°C, light rain" # run the real function here messages.append({"role": "assistant", "content": first.content}) messages.append({"role": "user", "content": [ {"type": "tool_result", "tool_use_id": use.id, "content": result}, ]}) final = client.messages.create(model="mindshub_air", max_tokens=1024, messages=messages, tools=tools) print(final.content[-1].text) ``` ```typescript Chat Completions const tools = [{ type: "function" as const, function: { name: "get_weather", description: "Get the current weather for a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"], }, }, }]; const messages: any[] = [{ role: "user", content: "What's the weather in Lisbon?" }]; const response = await client.chat.completions.create({ model: "mindshub_air", messages, tools }); const reply = response.choices[0].message; if (!reply.tool_calls?.length) throw new Error(`Expected a tool call, got prose: ${reply.content}`); const call = reply.tool_calls[0]; const result = "19°C, light rain"; // run the real function here messages.push(reply, { role: "tool", tool_call_id: call.id, content: result }); const final = await client.chat.completions.create({ model: "mindshub_air", messages, tools }); console.log(final.choices[0].message.content); ``` ```typescript Responses const tools = [{ type: "function" as const, name: "get_weather", description: "Get the current weather for a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"], }, }]; const first = await client.responses.create({ model: "mindshub_air", input: "What's the weather in Lisbon?", tools }); const call = first.output.find((item) => item.type === "function_call"); if (!call) throw new Error(`Expected a tool call, got prose: ${first.output_text}`); const result = "19°C, light rain"; // run the real function here const final = await client.responses.create({ model: "mindshub_air", previous_response_id: first.id, input: [{ type: "function_call_output", call_id: call.call_id, output: result }], tools, }); console.log(final.output_text); ``` ```typescript Messages const tools = [{ name: "get_weather", description: "Get the current weather for a city", input_schema: { type: "object" as const, properties: { city: { type: "string" } }, required: ["city"], }, }]; const messages: any[] = [{ role: "user", content: "What's the weather in Lisbon?" }]; const first = await client.messages.create({ model: "mindshub_air", max_tokens: 1024, messages, tools }); const use = first.content.find((block) => block.type === "tool_use"); if (!use) throw new Error("Expected a tool call, got prose"); const result = "19°C, light rain"; // run the real function here messages.push( { role: "assistant", content: first.content }, { role: "user", content: [{ type: "tool_result", tool_use_id: use.id, content: result }] }, ); const final = await client.messages.create({ model: "mindshub_air", max_tokens: 1024, messages, tools }); console.log(final.content.at(-1)); ``` Rules that hold on every API: - A single response can contain multiple tool calls. Execute them all and return one result per call. - Echo call ids back exactly as you received them. On Gemini models the ids carry provider state, and a mismatched id fails the request. - Send the same `tools` array on the follow-up call. - The reply can come back as prose instead of a call on any model, so the guard above is a real branch, not defensive noise. ### Response ```json Chat Completions { "role": "assistant", "content": null, "tool_calls": [ { "id": "toolu_01V9g8n42TGjbfJmvecVydN3", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\": \"Lisbon\"}"} } ] } ``` ```json Responses {"type": "function_call", "id": "fc_a91b", "call_id": "call_a91b", "name": "get_weather", "arguments": "{\"city\": \"Lisbon\"}"} ``` ```json Messages {"type": "tool_use", "id": "toolu_01V9g8n42TGjbfJmvecVydN3", "name": "get_weather", "input": {"city": "Lisbon"}} ``` On Chat Completions the turn ends with `finish_reason: "tool_calls"`; on Messages with `stop_reason: "tool_use"`; on Responses the `function_call` item sits in `output` and `status` is `completed`. Use `role: "tool"` for Chat Completions results (`role: "function"` is accepted for backward compatibility but not handled as a tool result). ### Streaming 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 call as a single frame carrying the complete arguments. Responses streams `response.function_call_arguments.delta` events between an `output_item.added`/`output_item.done` pair. Details in [Streaming](https://docs.mindshub.ai/inference/streaming.md). Remote MCP calls are not narrated on a stream yet: a streamed turn runs the same calls and returns the same answer, but emits no `mcp_*` events and, on Messages, no `mcp_tool_use` / `mcp_tool_result` blocks, so a streaming client has nothing to replay and the next turn fetches the server's tool list again. Non-streamed turns carry them in full. ### Controlling which tool runs `tool_choice` takes `"auto"`, `"required"` (`"any"` on Messages), `"none"`, or a named tool. Two behaviors are MindsHub-specific: - **`"none"` is honored everywhere.** On the Claude family it is sent as Anthropic's own `none`. On `mindshub_air`, `deepseek-v4-flash`, `qwen`, `glm`, and `muse-spark`, whose upstreams have no word for it, and on the Claude heads listed in the table below, it is honored by dropping the `tools` from that request instead, reported as `tool_choice=none>dropped` in `X-MindsHub-Clamped-Params`. - **A forced choice a model can't accept is rewritten rather than rejected.** The request is served instead of returning the provider's 400: | Model | What it restricts | What we send instead | |---|---|---| | `kimi` | A named choice 400s while the model's thinking is on, which is the provider default. `"required"` works. | Named → `"auto"` | | `muse-spark` | Only `"auto"` is accepted; named, `"required"`, and `"none"` all 400. | Named and `"required"` → `"auto"`; `"none"` honored by dropping `tools` | | `sonnet`, `opus`, `fable` and their twins `sonnet-5-5`, `opus-5-5`, `fable-5-1` | Claude Sonnet 5.5, Opus 5.5, and Fable 5.1 reject a forced choice (`any` or a named tool). The pins `sonnet-5`, `opus-5`, and `fable-5` accept it. | Named and `"required"` → `"auto"`; `"none"` honored by dropping `tools` | On these models a forced choice becomes a strong hint, so the model *may* answer in prose. Keep the "call the tool first" instruction in your prompt, and validate that you got a call before relying on one. The rewrite is reported on `X-MindsHub-Clamped-Params` as `tool_choice=required>auto`, `tool_choice=named>auto`, or `tool_choice=none>dropped`. `parallel_tool_calls` is honored wherever the model can express it, including on the Claude family, which spells it inverted (`disable_parallel_tool_use`). Gemini models can't express it and report it as dropped. ### Remote MCP servers Point the model at a remote [Model Context Protocol](https://modelcontextprotocol.io) server and the platform runs its tools for you, inside the turn, on every model: on Anthropic's connector for the Claude family, on OpenAI's and xAI's for the GPT and Grok families, and through the platform's own MCP client for Gemini and the open-weight models (`deepseek-v4-flash`, `qwen`, `glm`, `kimi`, `muse-spark`), which see the server's tools as ordinary functions and get their results back the same way. You declare the server in each API's own shape and get the calls and their results back in the response. The server must be reachable over `https` on a public address; a bearer token goes in the request and is never stored or traced. ```python Chat Completions response = client.chat.completions.create( model="sonnet", messages=[{"role": "user", "content": "Which wiki sections does anthropics/anthropic-sdk-python have?"}], tools=[{ "type": "mcp", "server_label": "deepwiki", "server_url": "https://mcp.deepwiki.com/mcp", "require_approval": "never", }], ) print(response.choices[0].message.content) ``` ```python Responses response = client.responses.create( model="sonnet", input="Which wiki sections does anthropics/anthropic-sdk-python have?", tools=[{ "type": "mcp", "server_label": "deepwiki", "server_url": "https://mcp.deepwiki.com/mcp", "require_approval": "never", }], ) for item in response.output: if item.type == "mcp_call": print(item.name, item.output[:80]) print(response.output_text) ``` ```python Messages response = client.beta.messages.create( model="sonnet", max_tokens=1024, messages=[{"role": "user", "content": "Which wiki sections does anthropics/anthropic-sdk-python have?"}], mcp_servers=[{"type": "url", "url": "https://mcp.deepwiki.com/mcp", "name": "deepwiki"}], tools=[{"type": "mcp_toolset", "mcp_server_name": "deepwiki"}], betas=["mcp-client-2025-11-20"], ) for block in response.content: if block.type == "mcp_tool_use": print(block.name, block.input) ``` ```typescript Chat Completions const response = await client.chat.completions.create({ model: "sonnet", messages: [{ role: "user", content: "Which wiki sections does anthropics/anthropic-sdk-python have?" }], // Chat Completions has no MCP tool of its own; this entry is a MindsHub extension. tools: [{ type: "mcp", server_label: "deepwiki", server_url: "https://mcp.deepwiki.com/mcp", require_approval: "never" } as any], }); console.log(response.choices[0].message.content); ``` ```typescript Responses const response = await client.responses.create({ model: "sonnet", input: "Which wiki sections does anthropics/anthropic-sdk-python have?", tools: [{ type: "mcp", server_label: "deepwiki", server_url: "https://mcp.deepwiki.com/mcp", require_approval: "never" }], }); for (const item of response.output) { if (item.type === "mcp_call") console.log(item.name, item.output?.slice(0, 80)); } console.log(response.output_text); ``` ```typescript Messages const response = await client.beta.messages.create({ model: "sonnet", max_tokens: 1024, messages: [{ role: "user", content: "Which wiki sections does anthropics/anthropic-sdk-python have?" }], mcp_servers: [{ type: "url", url: "https://mcp.deepwiki.com/mcp", name: "deepwiki" }], tools: [{ type: "mcp_toolset", mcp_server_name: "deepwiki" }], betas: ["mcp-client-2025-11-20"], }); for (const block of response.content) { if (block.type === "mcp_tool_use") console.log(block.name, block.input); } ``` **What comes back.** On Messages, each call is an `mcp_tool_use` block followed by its `mcp_tool_result` block, ahead of the text, on a Claude *or* a GPT target. On Responses, a non-streamed turn reports `mcp_list_tools` (the tools the server advertised) and one `mcp_call` item per call, ahead of the message; keep them in your next request's `input` (or chain with `previous_response_id`) and the server's tool list is not fetched again. Chat Completions has no slot for the calls: the answer reflects the tool's output and the body is unchanged. Send the previous turn back as usual on every API and the calls stay in the model's context. **Choosing tools.** OpenAI's `allowed_tools` (a list of names, or `{"tool_names": [...]}`) and Anthropic's `mcp_toolset` `default_config` / `configs` both work on every target. A deny-list (`configs: {"delete": {"enabled": false}}` under an enabled default) runs on Claude, Gemini and the open-weight models; GPT and Grok targets have no spelling for one, so such a server is dropped rather than run with the denied tools re-enabled, and named on `X-MindsHub-Dropped-Params`. Where the platform's own client runs the server, one request may declare at most 8 servers and each exposes at most 64 tools to the model; the rest are dropped and reported. A server whose tool list cannot be fetched is skipped for that turn and reported as an `mcp_list_tools` item carrying the error on Responses. **Approvals.** `require_approval` is read the way each API defines it, and a tool is never run ungated because a target could not gate it: | API | GPT target | Every other target (Claude, Grok, Gemini, open-weight) | |---|---|---| | Messages | `never` (the dialect has no approval field) | `never` | | Responses | OpenAI's semantics untouched, omitted included (`mcp_approval_request` item, answer with an `mcp_approval_response` on the next turn) | tools resolving to `never` run; the rest are dropped and named on `X-MindsHub-Dropped-Params` | | Chat Completions | explicit `"never"` required (the body cannot carry an approval request); otherwise dropped and named | same | **Billing.** No per-call charge. The server is yours and no one charges to call it; you pay the tokens the tool schemas and results add to the turn, which `usage` already reports. Calls are counted (`mcp_call`) on your usage summary. ### Legacy function calling (Chat Completions only) `functions` and `function_call` are OpenAI's original function-calling fields, deprecated in favour of `tools`/`tool_choice`. They still work here, in all four places OpenAI switches shape when you use them: | | You send / receive | |---|---| | Request | `functions: [{name, description, parameters}]`, `function_call: "auto" \| "none" \| {"name": ...}` | | Response | `message.function_call: {name, arguments}`, `finish_reason: "function_call"` | | Streaming | `delta.function_call`: the name on the opening chunk, then `arguments` fragments | | Replay | the assistant turn with its `function_call`, then `{"role": "function", "name": ..., "content": ...}` | Two limits: one call per turn (the legacy field is a single object, so extras are dropped), and sending `tools` alongside `functions` is read as a modern request and answered with `tool_calls`. New code should use `tools`. ### Per-model differences The forced-choice table above is the whole list for function tools. Remote MCP runs on every model: natively on the Claude, GPT and Grok families, through the platform's own client on Gemini and on `deepseek-v4-flash`, `qwen`, `glm`, `kimi` and `muse-spark`. OpenAI's forced `tool_choice: {"type": "mcp", ...}` is honoured on GPT and Grok targets and read as `auto` elsewhere, where the tools run at the model's discretion. Everything else about tool calling is uniform across the catalog. ### Errors you can hit A malformed tool schema returns the provider's own `400` naming the keyword at fault. A malformed `mcp` entry (no `server_url`, or one that is not `https`) is a `400` with `code: invalid_value` on Responses; on Messages an `mcp_toolset` whose `mcp_server_name` names no `mcp_servers` entry, or a server no toolset references, is a `400` as on Anthropic. A server that cannot be reached surfaces as the upstream's own error. A mismatched call id on Gemini models fails the follow-up request with a `400`. See [Errors](https://docs.mindshub.ai/inference/errors.md). --- ## Traces(2) `GET /v1/traces` returns a record of the requests your organization has made through the API — model, timing, token counts, and whether the request succeeded. `GET /v1/traces/{trace_id}` returns one of those records in full, with its span waterfall and payloads. This is health and debugging data, not billing. Token counts appear as request metadata; spend lives in your billing dashboard. Traces are available where your organization has them enabled. Everywhere else both endpoints answer `404`. ### Endpoint `GET https://api.mindshub.ai/v1/traces` ```python Python import os import httpx headers = {"Authorization": f"Bearer {os.environ['MINDSHUB_API_KEY']}"} page = httpx.get( "https://api.mindshub.ai/v1/traces", params={"page": 1, "limit": 25, "model": "sonnet"}, headers=headers, ).json() for trace in page["traces"]: print(trace["id"], trace["model"]) ``` ```typescript TypeScript const headers = { Authorization: `Bearer ${process.env.MINDSHUB_API_KEY}` }; const params = new URLSearchParams({ page: "1", limit: "25", model: "sonnet" }); const page = await fetch(`https://api.mindshub.ai/v1/traces?${params}`, { headers }).then((r) => r.json()); for (const trace of page.traces) console.log(trace.id, trace.model); ``` The generated reference pages are [GET /v1/traces](/api-reference/list-traces) and [GET /v1/traces/\{trace_id\}](/api-reference/get-trace). ### Who sees what Every response is scoped to your organization. Within it, what you see depends on your role: - **Members** see their own requests. - **Organization administrators** see the whole organization's requests, and can narrow to one person with `user_id`. Request and response **content** is always your own, for every role. An administrator can see that a colleague made a request, with its model, timing and outcome, but opening it returns `404`. There is no setting that changes this today. ### Request parameters All optional. | Parameter | Meaning | |---|---| | `page`, `limit` | 1-based page number and page size. `limit` defaults to 25, maximum 100. | | `from`, `to` | ISO-8601 instants bounding the request time. A value without an offset is read as UTC. | | `hours` | Legacy relative window. Combines with `from`/`to` when both are given; prefer the absolute pair. | | `session_id` | Only requests belonging to this session. | | `model`, `exclude_model` | Model aliases. Repeatable. | | `provider`, `exclude_provider` | Upstream providers. Repeatable. | | `level`, `exclude_level` | `DEFAULT` (succeeded), `ERROR` (failed), `UNKNOWN` (no outcome recorded). Repeatable, OR-ed. | | `stream` | `true` for streamed requests, `false` for non-streamed. | | `include_probes` | `true` to include probe requests (background calls a MindsHub client made on your key), which are hidden by default. | | `sort`, `order` | `timestamp` or `latency_ms`, `asc` or `desc`. Defaults to newest first. | | `user_id` | Administrators only: narrow to one person. Ignored for members. | Every filter runs server-side over your whole history, not over one page. The exclusions are null-permissive: excluding a model never hides requests whose model was not recorded. ### Response ```json { "traces": [ { "id": "3f1c8a90d4e24b7f9a6c5e2d", "name": "cowork:turn-3", "timestamp": "2026-09-16T09:41:22.118Z", "user_id": "0f2b5c71-9e3a-4d18-bb44-7c6a1d2e5f30", "session_id": "b7d1e4a2-5c88-4f30-9a61-2e7c4b9d8f15", "model": "sonnet", "provider": "anthropic", "input_tokens": 1840, "output_tokens": 302, "cached_input_tokens": 1536, "latency_ms": 4120, "stream": true, "level": "DEFAULT", "http_status": 200, "error_class": null, "span_count": 0, "kind": null, "preview": null, "tags": [] } ], "page": 1, "limit": 25, "total": 412, "total_capped": false, "has_more": true, "langfuse_available": true } ``` | Field | Meaning | |---|---| | `id` | The trace id. Pass it to `GET /v1/traces/{trace_id}`. A request recorded while tracing was off carries an internal id instead. That id opens only for a stored Responses turn, whose payload we hold ourselves, and only while the turn is live; every other untraced request returns `404`. | | `name` | What the request was, when we can say: the calling client and turn (`cowork:turn-3`), otherwise the operation (`chat.completion`, `response`, `embedding`, `decision`). | | `timestamp` | When the request started. | | `model`, `provider` | The alias you called, and the upstream that served it. | | `input_tokens`, `output_tokens`, `cached_input_tokens` | Token counts. `cached_input_tokens` are the discounted prompt-cache reads; they are not included in `input_tokens`. | | `latency_ms` | End to end, in milliseconds. For a streamed request this is time to the last token and includes your own read time. `stream` tells the two apart, so compare like with like. | | `level` | `DEFAULT` succeeded, `ERROR` failed, `null` when no outcome was recorded. | | `http_status`, `error_class` | The status we returned and the failure category, when recorded. | | `session_id` | Groups a multi-turn conversation. `null` for a request that carried no session header, which is most direct API traffic. | | `kind` | `probe` for a background call a MindsHub client made on your key, otherwise `null`. | | `span_count` | Always `0` on the list. The real count comes with the detail. | | `preview`, `tags` | Reserved. Always `null` and `[]` today. | | `total` | Matching requests, or `null` when it could not be counted cheaply — see below. | | `total_capped` | `true` when counting stopped at the cap, so `total` is `null` rather than approximate. | | `has_more` | Whether another page is reachable. Page on this, not on `total`. | | `langfuse_available` | Whether the tracing backend is configured in this environment. The list never depends on it, and a stored Responses turn still opens when it is `false` — from our own payload, so without its span waterfall. | #### Counts and paging `total` is exact or `null`, never an estimate. It is `null` in two cases: when more than 10,000 requests match, and when the query filters on anything other than time, because counting those cannot be done cheaply. `has_more` is always correct, so page on that. Paging stops at an offset of 10,000. Beyond it the endpoint returns `400`; narrow with `from`/`to` or `user_id` instead of paging deeper. ### Getting one trace `GET https://api.mindshub.ai/v1/traces/{trace_id}` ```python Python import os import httpx headers = {"Authorization": f"Bearer {os.environ['MINDSHUB_API_KEY']}"} trace = httpx.get("https://api.mindshub.ai/v1/traces/tr_1a2b3c", headers=headers).json() print(trace["level"], len(trace["spans"])) ``` ```typescript TypeScript const headers = { Authorization: `Bearer ${process.env.MINDSHUB_API_KEY}` }; const trace = await fetch("https://api.mindshub.ai/v1/traces/tr_1a2b3c", { headers }).then((r) => r.json()); console.log(trace.level, trace.spans.length); ``` The response is a list row plus `input`, `output`, `metadata`, and `spans` — the nested operations the request ran, each with its own timing, model, level and payloads, ordered by start time. ### Lane-specific behaviour **Background calls are hidden.** Some MindsHub clients make small calls on your key that you did not ask for: a connectivity check that verifies the key, or an internal comparison that runs alongside a request you did make. Those are marked as probes and excluded from the list by default so it reflects real activity. They are still billed and count toward your rate limits. Pass `include_probes=true` to see them; they drop out of the counts while hidden. **Detail can lag the list.** Trace ingestion is asynchronous, so a request that appears in the list immediately may not be retrievable by id for a few seconds. **Detail can expire before the list does.** The list is served from our own records and keeps its history. The span waterfall is held by the tracing backend under whatever retention it is configured with, and a stored Responses turn's payload is held by us for [30 days](https://docs.mindshub.ai/inference/conversation-state.md#server-side-state-on-responses). Neither is tied to how long the list keeps a request, so an old request can list correctly and still return `404` when opened. ### Errors you can hit | Status | When | |---|---| | `400` | Paging past the offset limit. Narrow the window instead. | | `401` | Missing or invalid key. | | `404` | Traces are not enabled for your organization; or the trace does not exist; or it is not yours to see. All three answer `404` rather than `403`, so a disabled surface is invisible rather than visibly gated. The last two are indistinguishable by design, down to the response body: we never confirm that someone else's trace exists. | | `503` | The trace is yours, but the detail backend is temporarily unavailable. The list keeps working. Retry the drill-down. | See [Errors](https://docs.mindshub.ai/inference/errors.md) for the response shape. --- ## MindsHub in VS Code VS Code's chat can talk to Chat Completions-compatible endpoints, MindsHub included, through Copilot's **Custom Endpoint** provider. MindsHub models appear in the model picker alongside the built-in ones. Copilot Chat is the only extension you need, and bring-your-own-key models work without a paid Copilot plan. ### Prerequisites - **VS Code 1.128 or newer.** The Custom Endpoint provider shipped in 1.122; per-model `modelOptions`, which Kimi K3 needs, arrived in 1.128. Check under **Code → About** (macOS) or **Help → About**. `code --version` also works, but only after installing the shell command (Command Palette: "Shell Command: Install 'code' command in PATH"). - The **GitHub Copilot Chat** extension. (Business and Enterprise organizations can disable BYOK by policy.) - A MindsHub API key from the [console](https://console.mindshub.ai). Create a dedicated key for VS Code so it's easy to revoke: it will live in a config file, not a secret store. ### Check your key first Before touching VS Code, confirm the key and endpoint work: ```bash curl https://api.mindshub.ai/v1/chat/completions \ -H "Authorization: Bearer $MINDSHUB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "kimi", "messages": [{"role": "user", "content": "say OK"}], "stream": true}' ``` A stream of `data:` lines means you're ready. A `401` means authentication failed. Fix that before configuring the editor, or you'll be debugging two things at once. ### Add the model 1. Open the Chat view, click the **model picker**, and choose **Manage Language Models**, or run **"Chat: Manage Language Models"** from the Command Palette. 2. Choose **Add Models**, then **Custom Endpoint**. 3. Enter a group name (`MindsHub`), a display name, and your API key, and pick **Chat Completions** as the API type. The key is written into the config file as plain text (see [Key handling](#key-handling)). 4. VS Code opens `chatLanguageModels.json` with the provider fields already filled in from step 3 (the group `name`, `vendor`, `apiKey`, and `apiType`). Add a `models` entry and save: ```json [ { "name": "MindsHub", "vendor": "customendpoint", "apiKey": "mdb_your_key_here", "apiType": "chat-completions", "models": [ { "id": "kimi", "name": "Kimi K3 (MindsHub)", "url": "https://api.mindshub.ai/v1/chat/completions", "toolCalling": true, "vision": false, "contextWindow": 200000, "maxOutputTokens": 16000, "modelOptions": { "temperature": 1 } } ] } ] ``` 5. Pick your model from the chat model picker and send a message. Important model-entry fields: - **`id`** is the [catalog alias](https://docs.mindshub.ai/inference/models.md) sent as the `model` field: `kimi`, `gpt-mini`, `gemini-flash`, `sonnet`. Not a provider model ID. - **`url`** is the endpoint VS Code posts to. Use the full path ending `/v1/chat/completions`; a bare `/v1` returns `404 {"detail":"Not Found"}`. - **`modelOptions`** merges extra parameters into every request (VS Code 1.128+). **Kimi requires it**: VS Code otherwise sends `temperature: 0.1`, which Kimi rejects with a 400. `"temperature": 1` makes Kimi work. Most other catalog models accept the default and don't need this block. - **`toolCalling`** must be `true` for the model to appear in **agent mode**. `vision: true` likewise gates image attachments. - **`contextWindow`** and **`maxOutputTokens`** are budgets VS Code plans against, not values read from the API. Without `contextWindow`, VS Code treats `maxInputTokens + maxOutputTokens` as the window, so set the window explicitly. The 200,000 example is conservative for every current catalog model; raise it to your model's real window for longer sessions. Each entry in the `models` array appears separately in the picker. The top-level `name` groups them; each model's own `name` is its label. ### Set a default model To make a MindsHub model the default for new chats, add this to `settings.json`. The value is the model's `id`: ```json { "chat.defaultModel": "kimi" } ``` ### Skipping the wizard The wizard writes `chatLanguageModels.json`, and you can write that file yourself and restart VS Code. In a Stable install with the default profile it lives beside `settings.json`: | Platform | Path | |---|---| | macOS | `~/Library/Application Support/Code/User/chatLanguageModels.json` | | Linux | `~/.config/Code/User/chatLanguageModels.json` | | Windows | `%APPDATA%\Code\User\chatLanguageModels.json` | Profiles, Insiders, and portable installs use different roots. When in doubt, let **Manage Language Models** open the right file for you. ### Key handling `apiKey` holds the literal key, and VS Code sends whatever string is there as the bearer token. Use a dedicated, revocable key, and keep the file out of commits, Settings Sync, backups, and screenshots. VS Code's documentation recommends an input variable (`"apiKey": "${input:myApiKey}"`) so the key lives in secret storage instead. On VS Code 1.131.0 with Copilot Chat 0.59.0 that placeholder was passed through unexpanded, producing an empty bearer token and a `401` on every request. Treat the literal key as the working approach for now and retest the input variable on later versions. ### Scope and limits - **Chat and agent mode both work**, including tool calling on models that declare it. BYOK models can also serve VS Code's configurable utility tasks; inline code completions, semantic search, and embeddings still require GitHub Copilot. - **Copilot sends `temperature: 0.1` unless the model entry overrides it** via `modelOptions`. - **Per-request token counts don't reach VS Code.** On chat completions MindsHub emits the streaming usage chunk only when a request sets `stream_options: {"include_usage": true}`, and VS Code doesn't send it; its context meter is a local estimate either way. Your [usage summary](https://docs.mindshub.ai/inference/billing.md#checking-usage-and-balance-from-code) is what bills. - **Costs land in MindsHub.** VS Code shows no cost for custom endpoints. ### Troubleshooting | Symptom | Cause and fix | |---|---| | Model missing from the picker | VS Code reads `chatLanguageModels.json` at startup. Restart it, then confirm `"vendor": "customendpoint"` and valid JSON. On VS Code older than 1.122 the file is ignored entirely; check the version in the About dialog. | | Model missing in **agent** mode only | Add `"toolCalling": true` to that model and restart. | | `400 invalid temperature` | The model rejects VS Code's default `temperature: 0.1`. Add `"modelOptions": {"temperature": 1}` to the model entry (VS Code 1.128+). | | `404` with `model_not_found` | `id` isn't a catalog alias. Check [`GET /v1/models`](https://docs.mindshub.ai/inference/models.md). | | `404` with `{"detail":"Not Found"}` | `url` doesn't end in `/v1/chat/completions`. | | `401` | Authentication failed: the key is wrong, revoked, or `apiKey` still holds an unexpanded `${input:...}` placeholder. | *Verified with VS Code 1.131.0 and Copilot Chat 0.59.0 in July 2026: models loaded from a hand-written config, Kimi K3 round trip with `modelOptions` temperature, and streamed responses in the chat view.* --- ## Web search Add the built-in `web_search` tool and the platform handles retrieval: models with a native search index use it, and models without one search through MindsHub's own loop. There is no search provider to configure and no separate key. ### Works with Models whose catalog entry declares search. On a model without search the web tools are dropped without warning, so the call looks like it worked while the model answered from its own knowledge; check the per-model column on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models) before relying on it. ### Request ```python Chat Completions response = client.chat.completions.create( model="sonnet", messages=[{"role": "user", "content": "What shipped in Python 3.14?"}], tools=[{"type": "web_search"}, {"type": "fetch"}], ) print(response.choices[0].message.content) ``` ```python Responses response = client.responses.create( model="sonnet", input="What shipped in Python 3.14?", tools=[{"type": "web_search"}], ) print(response.output_text) ``` ```python Messages message = client.messages.create( model="sonnet", max_tokens=1024, messages=[{"role": "user", "content": "What shipped in Python 3.14?"}], tools=[{"type": "web_search_20250305", "name": "web_search"}], ) print(message.content[-1].text) ``` ```typescript Chat Completions const response = await client.chat.completions.create({ model: "sonnet", messages: [{ role: "user", content: "What shipped in Python 3.14?" }], tools: [{ type: "web_search" }, { type: "fetch" }] as any, }); console.log(response.choices[0].message.content); ``` ```typescript Responses const response = await client.responses.create({ model: "sonnet", input: "What shipped in Python 3.14?", tools: [{ type: "web_search" }], }); console.log(response.output_text); ``` ```typescript Messages const message = await client.messages.create({ model: "sonnet", max_tokens: 1024, messages: [{ role: "user", content: "What shipped in Python 3.14?" }], tools: [{ type: "web_search_20250305", name: "web_search" }], }); console.log(message.content.at(-1)); ``` These samples use `sonnet`, which searches natively and draws the prepaid wallet. - `web_search` lets the model run web searches; `fetch` (Chat Completions) lets it retrieve a URL. On Messages, Anthropic's hosted `web_search*` and `web_fetch*` tool types map onto the same platform search. - OpenAI's `web_search_options` parameter on Chat Completions does the same thing as a `{"type": "web_search"}` tool entry. Its `search_context_size` and `user_location` sub-options have no cross-provider equivalent and are dropped. - 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. - Searches carry a per-search charge on top of tokens; see [Billing](https://docs.mindshub.ai/inference/billing.md#web-search-and-fetch). ### Response A turn that searched reports its sources where each API puts them: ```json Chat Completions {"type": "url_citation", "url_citation": {"start_index": 42, "end_index": 118, "title": "EU AI Act — Article 6", "url": "https://example.eu/ai-act"}} ``` ```json Responses {"type": "web_search_call", "id": "ws_1a2b", "status": "completed"} ``` ```json Messages {"type": "web_search_result_location", "url": "https://example.eu/ai-act", "title": "EU AI Act — Article 6", "cited_text": "…", "encrypted_index": "mindshub:…"} ``` - **Chat Completions**: `message.annotations` in OpenAI's `url_citation` shape; the offsets index into `message.content`. - **Responses**: `web_search_call` items in `output`, and `url_citation` annotations on the `output_text` part. Models that don't attribute sources return `annotations: []`. `include` doesn't gate any of this: `web_search_call.results` and `web_search_call.action.sources` are accepted and ignored; the items and annotations are returned either way. - **Messages**: `citations` on the `text` block they apply to, in Anthropic's `web_search_result_location` shape, including when the target model is a GPT or Grok one, whose offset citations are converted to the quoted span this wire format uses. `encrypted_index` is Anthropic's own opaque replay cursor, so a citation relayed from a non-Anthropic model carries a synthesized `mindshub:`-prefixed value there. Only some models report sources: the GPT/Grok and Claude families tell us which sentence used which page, while Kimi's builtin search, the external search loop (Fireworks, Meta), and Gemini's own grounding hand results to the model without marking which sentences drew on them. Those answers omit citations entirely rather than guessing. Gemini's grounding is invisible entirely: it runs and bills like any other provider's search but produces neither a `web_search_call` item nor citations, so the only signal a search happened is the charge for it. ### Streaming Streaming does not narrate searches yet: a streamed Responses turn runs the same searches and returns the same annotations on its terminal `response.completed` event, but emits no `response.web_search_call.*` events along the way. Native-search models otherwise stream normally. Models that search through the external loop (`deepseek-v4-flash`, `qwen`, `glm`, `muse-spark`, and `kimi` when routed externally) run the loop first and replay the finished answer as a stream, so the first token arrives late. See [Streaming](https://docs.mindshub.ai/inference/streaming.md). ### Per-model differences | Provider | How it searches | |---|---| | Anthropic (Claude family) | Native `web_search` and `web_fetch` tools | | OpenAI and xAI (GPT and Grok families) | Native `web_search` tool | | Gemini | Google Search grounding; `fetch` is always dropped, and no search items or citations are reported | | Moonshot (`kimi`) | Its `$web_search` builtin, or the external loop, per policy | | Fireworks and Meta (`deepseek-v4-flash`, `qwen`, `glm`, `muse-spark`) | External loop (Exa) only | Which mode each alias uses today is on the [capability matrix](https://docs.mindshub.ai/inference/capability-matrix.md#models); it can change by policy without a release. ### Errors you can hit None specific to this feature. Search charges are refused up front like any other paid usage when the wallet is empty (`402 wallet_empty`). On `mindshub_air` a search draws the included allowance first, so it needs no wallet until the allowance is spent. See [Errors](https://docs.mindshub.ai/inference/errors.md) and [Billing](https://docs.mindshub.ai/inference/billing.md).