Skip to content
Heimdallm
Marketplace/Documentation
Documentation · API reference

Chat completions

POST https://api-staging.impossiblecarrot.cc/api/v1/chat/completions

The primary inference endpoint. OpenAI-compatible request and response; requires a bearer API key. Heimdallm routes the request to one of the model's live endpoints — see Models & routing for how, and how to steer it.

Request body

FieldTypeNotes
modelstringModel id, optionally with :free / :nitro / :floor. Required unless models is given.
modelsstring[] (≤ 20)Fallback model list, tried in order after model.
messagesobject[] (≥ 1)The conversation; message shapes below. Required.
streambooleantrue switches the response to server-sent events.
stream_optionsobjectinclude_usage is accepted for compatibility; the final usage chunk is sent either way. include_tool_dispatch: true opts into call-ready events. The options are omitted upstream.
temperaturenumber0 to 2.
top_pnumberGreater than 0 and at most 1. Zero returns 400; it is not replaced with a default.
top_kinteger≥ −1; -1 disables this filter where the endpoint declares it.
min_pnumber0 to 1.
top_anumberAccepted for compatibility; omitted from upstream chat requests.
frequency_penaltynumber−2 to 2.
presence_penaltynumber−2 to 2.
repetition_penaltynumberGreater than 0 and at most 2; requires endpoint declaration.
seedintegerBest-effort determinism where the endpoint supports it.
stopstring / string[]Up to 8 stop sequences.
max_tokensintegerCompletion ceiling, 1 to 1,048,576. Also bounds the credit hold.
max_completion_tokensintegerOpenAI's newer name for the same ceiling, 1 to 1,048,576.
logit_biasobjectToken id → bias in [-100, 100].
logprobsbooleanReturn log-probabilities where supported.
top_logprobsinteger0 to 20.
response_formatobject{"type":"text"}, {"type":"json_object"} or {"type":"json_schema", ...} — see Structured outputs.
structured_outputsbooleanAdvertised capability flag used with provider.require_parameters.
toolsobject[]Up to 1,536 function tool definitions; names match [a-zA-Z0-9_-]{1,128} — see Tool calling.
tool_choicestring / object"none", "auto", "required", or {"type":"function","function":{"name":"..."}}.
parallel_tool_callsbooleanAllow multiple tool calls in one turn.
reasoningobject{ "effort": "none"/"minimal"/"low"/"medium"/"high"/"xhigh"/"max", "max_tokens": n, "exclude": bool, "enabled": bool }.
reasoning_effortstringShorthand for reasoning.effort.
include_reasoningbooleanAccepted compatibility flag; the generic OpenAI-compatible adapter does not forward it.
verbositystringAccepted for compatibility; omitted from upstream chat requests.
nintegerOnly 1 is accepted.
userstring (≤ 256)Opaque end-user identifier, forwarded to the provider.
session_idstring (≤ 256)The conversation this turn belongs to — see Session affinity. Outranks the X-Session-Id header.
prompt_cache_keystring (≤ 256)Cache affinity hint used when session_id is absent. Outranks X-Session-Id; delivered to the provider as that header, not as a body parameter.
metadataobjectOpaque request metadata.
modalitiesstring[]What the reply may carry: text, image, audio — see Generated images and audio.
image_configobjectHow a generated image is drawn: n, resolution, aspect_ratio, size, quality, output_format, background, seed.
audioobject{ "voice": "...", "format": "wav" } — both members required for generated speech.
providerobjectProvider preferences.
routestringDeprecated routing alias: fallback groups by model; sort orders across models. Explicit provider.sort.partition takes precedence.
transformsstring[]["middle-out"] asks for middle-out compression; [] declines it, as an absent member does.
pluginsobject[]The same ask written as a plugin, [{ "id": "context-compression" }]. Any other id returns 400 naming it.
usageobject{ "include": true }, accepted for OpenRouter compatibility; usage.cost is returned either way.

The OpenAI-compatible adapter sends known chat fields. Standard sampling fields, output limits, tools, their modifiers and response_format are forwarded when supplied. The extensions top_k, min_p and repetition_penalty are sent only when the endpoint declares them in supported_parameters. Unknown body members are accepted for compatibility and omitted upstream.

reasoning.effort becomes reasoning_effort when the endpoint declares reasoning or reasoning_effort; the shorthand takes precedence when both spellings are supplied. For Anthropic endpoints, reasoning.max_tokens supplies the thinking budget. The generic OpenAI-compatible adapter does not forward reasoning.enabled, reasoning.max_tokens or reasoning.exclude; accepting those fields does not establish that it controls thinking or hides reasoning. The top-level reasoning, include_reasoning, structured_outputs, top_a and verbosity members are never sent upstream. Marketplace model selection, routing preferences, metadata and response usage preferences are consumed by the gateway. Streaming requests always ask the upstream for usage.

Two of them are routing inputs on their own. A request using tools or structured_outputs is only ever sent to an endpoint whose supported_parameters declares it, because an endpoint that ignores them answers the wrong kind of question — prose where you needed a tool call or JSON — and you would only find out after paying for the answer. Both declarations are verified rather than taken: an endpoint is asked to call a tool, and asked for a reply conforming to a schema, and one whose latest check failed has that promise withheld until a later check passes. Every explicitly requested optional setting requires an endpoint declaration. If no endpoint advertises all the requested settings, routing refuses the request before contacting a provider.

provider.options carries a provider's own knobs, keyed by provider slug, for the ones the normalized vocabulary has no name for. Only the key matching the provider that served the request is read, so naming options for several candidates is not an error. Each name must be one that endpoint declares as a passthrough parameter, and the value must satisfy the descriptor it declared for the name; an undeclared name or an inadmissible value is a 400 naming it rather than a request sent on and rejected upstream. A declared name reaches the provider unchanged.

reasoning_effort is matched against each endpoint's declared effort domain. A known domain that omits the requested value excludes that endpoint before dispatch; the gateway does not map it to a nearby level. Each declared value is checked separately, and a current declared level whose latest check failed is also excluded. If no endpoint remains eligible, the request returns 503.

A declared level without a verdict remains eligible. An unknown domain does not claim all seven values: the endpoint must declare the parameter, but its unspecified value domain does not prove every effort works. reasoning.effort and its reasoning_effort shorthand route identically. Check the endpoint's reasoning metadata for its declared levels and defaults.

reasoning_effort: "none" is held to the reply rather than to the status. An engine that accepts none and thinks anyway spends that thinking from the same budget that has to hold your answer, and bills you for it, so an endpoint whose latest none check found reasoning in the reply stops receiving requests that name none. This is visible only where an engine reports its reasoning on a channel of its own: one that writes deliberation into the answer text is indistinguishable from one that is simply verbose.

tool_choice: "required" (or a named function) is enforced as a contract: a response without a tool call cannot satisfy the request, so an endpoint answering prose to one fails its attempt and the request moves to the next endpoint. When no endpoint produces a tool call, the request fails with a 502 naming the contract — never silent prose. With tool_choice: "auto" a prose answer is often the right answer, and nothing is enforced.

max_tokens is a routing input too. An endpoint that can emit that many tokens at all — its declared max_completion_tokens, or its context window when it declares none — is preferred over one that cannot. Where no live endpoint of the model reaches the ceiling, the request is served by one that does not, with the ceiling lowered to that endpoint's own: a ceiling bounds the reply, so a coding harness asking for 64,000 tokens of a model whose endpoints emit at most 32,768 gets a reply of up to 32,768 rather than a 503. max_tokens and max_completion_tokens are one ceiling, and an endpoint declaring either name is sent the ceiling under the name it declared.

Within an eligible endpoint the ceiling is trimmed, not refused. An engine reserves the completion budget before it starts, so your prompt plus max_tokens has to fit in the window; most endpoints publish the same number for their window and their output ceiling, which makes their advertised maximum reachable only beside an empty prompt. Asking for it with a real prompt gives you the room that is actually there instead of a rejection. usage reports what was generated, as always.

Your prompt, for that arithmetic, is everything the engine tokenises: the messages and the tools schemas together. A coding harness sending forty tool declarations beside a short question is sending most of a window before the question, and the ceiling is trimmed against the whole of it. The same holds on /v1/messages and /v1/responses, whose max_tokens and max_output_tokens reach the same plan.

Asking for compression changes which side gives way. transforms: ["middle-out"] says your history should shorten so the ceiling survives, so an endpoint whose window cannot hold the part of the conversation compression can never drop beside the full ceiling is not a candidate for it. See middle-out prompt compression.

Messages

RoleShape
system / developer{ "role", "content", "name"? }
user{ "role", "content", "name"? } — content is a string or an array of content parts.
assistant{ "role", "content"?, "tool_calls"?, "reasoning"?, "reasoning_details"?, "refusal"?, "name"? }
tool{ "role", "content", "tool_call_id" } — a tool result, answering one tool_calls entry.

Array content parts: {"type":"text","text":"..."}, {"type":"image_url","image_url":{"url":"...","detail":"auto"}} (see Images & vision), {"type":"video_url","video_url":{"url":"..."}}, {"type":"audio_url","audio_url":{"url":"..."}}, {"type":"input_audio","input_audio":{"data":"...","format":"wav"}} and {"type":"file","file":{"filename":"...","file_data":"..."}}.

A file part carries its document as file_data, or names one stored with the Files API as {"type":"file","file":{"file_id":"file_..."}}, never both. The marketplace reads the stored bytes under your account before routing and sends the endpoint a data URL in their place, so an id is never forwarded; see Documents & files. An id your account cannot read, whether another account's, deleted or expired, is answered 404.

A *_url part carries either a URL the endpoint fetches or a data: URL carrying the bytes themselves, so the same part shape covers media you point at and media you have in hand. Each part is routed on what it needs: only endpoints declaring an input for that modality are eligible, and where an endpoint declares which sources (url, base64) and which containers it takes, a part outside them is not sent there. A part behind an https: URL names no container, so a container rule has nothing to judge and withholds nothing.

input_audio and audio_url are the same audio under two spellings, and engines differ over which they read. Send either: an endpoint that takes audio as inline bytes is handed an input_audio part rewritten as an audio_url data URL, with the same bytes in it. An endpoint that fetches its audio cannot be handed bytes at all, and a turn carrying them is refused rather than sent to fail.

When no endpoint of the model accepts a part the turn carries, the request answers 503 and the rejection names the modality — This endpoint declares no video input.

Reasoning details

Beside the flattened reasoning string, an endpoint that reports structured deliberation carries it in reasoning_details[] — on message in a non-streamed reply, on delta per chunk in a streamed one — as one of three entry types:

TypeShapeMeaning
reasoning.text{ "type", "text", "signature"? }Reasoning text, with a provider's signature over it where one is issued.
reasoning.summary{ "type", "summary" }A condensed account in place of the reasoning text itself.
reasoning.encrypted{ "type", "data" }An opaque block a provider hands back without exposing its content.

Every entry may also carry format (the originating dialect, e.g. anthropic-claude-v1) and index (its position within the reply) wherever the adapter that produced it has one to give. Streamed entries share an index; concatenating same-index reasoning.text fragments and taking whichever fragment carries a signature reconstructs the entry a non-streamed reply sends whole. reasoning_details is absent, never an empty array, wherever the endpoint reported nothing structured.

To continue a conversation without losing a provider's ability to verify or replay a turn's deliberation, send the previous reply's reasoning_details back unchanged on the assistant message that carried it — the exact sequence, unmodified and unreordered:

{
  "role": "assistant",
  "content": "…",
  "reasoning_details": [{ "type": "reasoning.text", "text": "…", "signature": "…" }]
}

Generated images and audio

modalities names what the reply may carry. Absent, it takes the endpoint's own default, which for a text model is prose. Naming image or audio asks for generated media beside (or instead of) the text, and the reply carries it in members of its own:

MemberShapeMeaning
message.images[]{ "type", "image_url": { "url" }, "index"? }One generated picture, as a data URL.
message.audio{ "id", "data", "expires_at", "transcript" }Generated speech: base64 data in the container you asked for.
delta.images[] / delta.audiothe same members, per chunkStreamed. A picture arrives whole; audio arrives fragment by fragment.

An image URL is a data URL wherever the endpoint handed back bytes: the marketplace stores no media, so there is no object it could put behind a URL of its own. Where the endpoint answered with a URL of its own, that URL is carried through unchanged rather than fetched.

audio requires both a voice and a format (wav, mp3, flac, opus, pcm16 or aac) — neither has a default, because filling one in would pick a voice you never chose. Which voices and which containers an endpoint offers is the endpoint's own; one that cannot speak refuses the request rather than answering it with prose.

A reply whose whole answer was the picture carries content as the empty string, never null: the turn was answered, and usage bills it the way it bills any other. Streamed, an image rides in one chunk's delta.images and nothing on that chunk's text channel; the audio's data and transcript accumulate across chunks, so concatenating each fragment's slice reconstructs what a non-streamed reply sends whole.

Routing. modalities is a routing promise, not a hint. A turn naming image or audio reaches only endpoints whose declared output modalities include it, so a request nobody serving the model can draw or speak comes back as a 503 naming the modality no endpoint declares — rather than as prose from an engine that accepted the ask and had no tower for it. Endpoints of one model differ here: a provider declares what each of its deployments produces, and the model's own list answers for a deployment whose document declares nothing.

Billing. Generated media is charged beside the tokens, on the endpoint's published price for it: image_megapixel per megapixel drawn, audio_second per second spoken. Neither figure comes from the request or from the endpoint's own summary of its work — the reply's bytes are read, so a picture pays for the pixels its container states and speech pays for the seconds its container accounts for. usage.cost carries the total as always, and the pixels and seconds behind it are on the generation record your account's activity reads. A unit an endpoint publishes no price for is recorded as unpriced and charged nothing; it is never billed at a price the marketplace chose on the provider's behalf.

{
  "modalities": ["image", "text"],
  "image_config": { "aspect_ratio": "16:9" },
  "messages": [{ "role": "user", "content": "Draw a lighthouse at dusk." }]
}

Example

curl https://api-staging.impossiblecarrot.cc/api/v1/chat/completions \
-H "Authorization: Bearer $HEIMDALL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "model": "deepseek/deepseek-v4-flash-0731-abliterated",
  "messages": [
    { "role": "system", "content": "You are terse." },
    { "role": "user", "content": "Name three prime numbers." }
  ],
  "temperature": 0.2,
  "max_tokens": 100
}'
{
  "id": "gen-9f2c7e1a4b6d",
  "object": "chat.completion",
  "created": 1755300000,
  "model": "deepseek/deepseek-v4-flash-0731-abliterated",
  "provider": "ImpCC",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "2, 3, 5." },
      "finish_reason": "stop",
      "native_finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 21,
    "completion_tokens": 9,
    "total_tokens": 30,
    "cost": 0.0000057
  }
}

Response fields

FieldTypeNotes
idstringThe generation id (gen-…); also sent as the X-Generation-Id header.
objectstringchat.completion.
createdintegerUnix seconds.
modelstringThe model that served the request (matters with fallbacks).
providerstringThe serving provider's display name. (Heimdallm)
choices[].indexintegerAlways 0 — n is fixed at 1.
choices[].messageobjectrole, content, and optionally reasoning, reasoning_details, refusal, tool_calls, images, audio.
choices[].finish_reasonstringstop, length, tool_calls, content_filter or error.
choices[].native_finish_reasonstringThe provider's raw finish reason, unnormalised.
usage.prompt_tokensintegerPrompt tokens, cached ones included — omitted where the endpoint reported no count.
usage.completion_tokensintegerCompletion tokens, reasoning included — omitted on the same terms.
usage.total_tokensintegerSum of the two, present only where both are.
usage.prompt_tokens_detailsobjectcached_tokens, cache_write_tokens — present when caching occurred.
usage.completion_tokens_detailsobjectreasoning_tokens — present when the endpoint reported a count.
usage.costnumberExact charge in USD. (Heimdallm)

Headers

Optional request headers attribute traffic to your app:

HeaderMeaning
X-OpenRouter-TitleYour app's name; recorded on the generation and returned as app_id by GET /generation.
HTTP-RefererYour app's URL, recorded alongside it.
X-Session-IdAn opaque conversation id (up to 256 characters) keying session affinity: requests carrying the same id prefer the endpoint that served the conversation, keeping its prompt cache warm. The session_id body member outranks it.

A non-empty X-OpenRouter-Title takes precedence over X-Title. The X-Title alias supplies the app name when X-OpenRouter-Title is absent or empty.

Response headers:

HeaderMeaning
X-Generation-IdThe generation id, sent on every response — including errors, so failures can be correlated.
X-Provider-NameThe serving provider (non-streaming responses; a stream carries provider in each chunk).

Streaming

With "stream": true the response is text/event-stream. Frame anatomy, in order:

: HEIMDALL PROCESSING

data: {"id":"gen-9f2c7e1a4b6d","object":"chat.completion.chunk","created":1755300000,"model":"deepseek/deepseek-v4-flash-0731-abliterated","provider":"ImpCC","choices":[{"index":0,"delta":{"role":"assistant","content":"2, 3"},"finish_reason":null}]}

data: {"id":"gen-9f2c7e1a4b6d","object":"chat.completion.chunk","created":1755300000,"model":"deepseek/deepseek-v4-flash-0731-abliterated","provider":"ImpCC","choices":[{"index":0,"delta":{"content":", 5."},"finish_reason":"stop"}]}

data: {"id":"gen-9f2c7e1a4b6d","object":"chat.completion.chunk","created":1755300000,"model":"deepseek/deepseek-v4-flash-0731-abliterated","provider":"ImpCC","choices":[],"usage":{"prompt_tokens":21,"completion_tokens":9,"total_tokens":30,"cost":0.0000057}}

data: [DONE]
  • Lines starting with : are keep-alive comments, sent about every 15 seconds while the model is thinking. Ignore them.
  • Each unnamed data: line except the last is a chat.completion.chunk: content arrives in choices[0].delta, and the chunk that ends generation carries a non-null finish_reason.
  • The final data frame before [DONE] has an empty choices array and the complete usage object, including cost.
  • data: [DONE] terminates the stream.

Call-ready tool dispatch

With "stream": true, a non-empty tools array and "stream_options": { "include_tool_dispatch": true }, the router selects only endpoints with a current positive tool_call_dispatch capability check. A qualifying upstream may send a named event before generation ends:

event: tool_call_dispatch
data: {"choice_index":0,"tool_call":{"index":0,"id":"call-1","type":"function","function":{"name":"calculator","arguments":"{\"expression\":\"42 * 17\"}"}}}

The tool_call is complete and its function.arguments is a JSON string. Ordinary delta.tool_calls fragments still arrive. Match choice_index and the call's index and id to treat the event and deltas as one logical call; do not concatenate the complete arguments onto streamed fragments or run the same call twice. Duplicate dispatch events for one identity are suppressed. Malformed, partial or dispatch payloads over 64 KiB are ignored. The gateway does not execute tools or guarantee exactly-once effects outside the stream. A failure after dispatch ends the stream without replaying the request on another endpoint. Usage and billing follow the ordinary streamed completion.

Clients without the opt-in receive only standard completion chunks. The Messages and Responses routes reject this extension.

Errors while streaming

A failure before any frame is written is an ordinary HTTP error with a real status code. Once the stream has started (first frame, or the first keep-alive after roughly one keep-alive period), the status is fixed at 200 and a failure arrives in-band as a final chunk with an error member, followed by data: [DONE]. A provider that fails before delivering any of the answer is retried on the model's other endpoints instead of surfacing here, so an in-band error means the answer had already begun, or that no endpoint could produce one:

data: {"id":"gen-9f2c7e1a4b6d","object":"chat.completion.chunk","created":1755300000,"model":"deepseek/deepseek-v4-flash-0731-abliterated","provider":"ImpCC","error":{"code":502,"message":"Provider disconnected","metadata":{"error_type":"provider_error"}},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}

data: [DONE]

Treat any chunk with an error member as terminal. The full parsing recipe is in the streaming guide.

Errors

400 invalid body · 401 bad key · 402 insufficient credits or spend limit · 404 unknown model · 413 body over 8 MB · 429 rate limited · 502/503/504 provider-side failure after all eligible endpoints were tried. Envelope and full status table: Errors & rate limits.

DiscoverFind your next modelModel libraryAll models, versions, prices and capabilitiesBenchmarksMeasured quality, cost and time by serving configurationShowcaseImages, video and 3D made on the marketplaceProvidersCompare independent inference providersActivityMarketplace traffic and usageNetwork statusAvailability, latency and endpoint healthChatConversations, model settings and filesImage, video & 3DGenerate images, video and 3D assetsScene studioStage a shot in 3D, direct it and render it as a videoCoding playgroundBuild software with an AI agent on a machine of your ownOverview & routingIdentity, workspaces and routing preferencesCredits & billingBalance, deposits, ledger and reserved fundsAPI keysKeys scoped to workspaces, limits and expirationOrganizationsTeams, members and shared workspacesRequest historyRequests, receipts, sandbox sessions and account historyReferralsInvitations and referral earningsPrivacyData settings, exports and deletionDocumentationQuickstart, SDKs, pricing and guidesAPI referenceInteractive endpoint reference