Chat completions
On this page
Chat completions generate the next assistant message in a conversation. The endpoint follows the OpenAI chat-completions API, so the OpenAI SDKs work against it; the compatibility notes list the differences.
Create a chat completion
/v1/chat/completions
Send the conversation so far and receive the assistant's reply, either as one JSON object or as a stream of events. Needs an API key.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
model |
string | No | The model to use. Defaults to Qwen/Qwen2.5-7B-Instruct. The model catalog lists the others. |
messages |
array | Yes | The conversation, oldest message first. Between 1 and 512 messages. |
messages[].role |
string | Yes | Who wrote the message: system, user, assistant or tool. |
messages[].content |
string, array or null | Yes | The message text. An array of content parts is accepted: the text of each part is joined with line breaks, and parts without text, such as images, are dropped. Only an assistant message with tool_calls may have null content, or leave it out. |
messages[].tool_calls |
array | No | The calls an assistant message made, as the model returned them. Up to 128. |
messages[].tool_calls[].id |
string | Yes | The call's ID. The tool message that answers it repeats it in tool_call_id. |
messages[].tool_calls[].type |
string | No | function. |
messages[].tool_calls[].function.name |
string | Yes | The function that was called. |
messages[].tool_calls[].function.arguments |
string | Yes | Its arguments, as JSON text. |
messages[].tool_call_id |
string | On tool messages |
The ID of the call a tool message answers. Its content is what the function returned. |
stream |
boolean | No | Whether to stream the reply as server-sent events. Defaults to true, unlike the OpenAI API. |
stream_options.include_usage |
boolean | No | When streaming, send one more chunk with token usage before the stream ends. Defaults to false. |
max_tokens |
integer | No | The most tokens to generate. Must be a positive whole number. Defaults to 1,024, and is lowered when the context window has less room left. A deployment that forwards to a model server passes it on, and the model server ends the reply there. |
user |
string | No | Names the session this request belongs to when X-Mortar-Session isn't sent. See cache-friendly prompts. |
tools |
array | No | Functions the model may call, up to 128. See call tools. |
tools[].type |
string | Yes | function. |
tools[].function.name |
string | Yes | 1 to 64 letters, digits, underscores or dashes, different for each tool. |
tools[].function.description |
string | No | What the function does, for the model to read. |
tools[].function.parameters |
object | No | A JSON Schema object describing the arguments. |
tools[].function.strict |
boolean | No | Asks a model server that supports it to follow parameters exactly. |
tool_choice |
string or object | No | auto, the default when there are tools, lets the model decide. none stops it calling any, required makes it call at least one, and {"type": "function", "function": {"name": "get_weather"}} makes it call that one. Needs tools. |
parallel_tool_calls |
boolean | No | Whether the model may make more than one call in a reply. Needs tools. |
Mortar ignores any other field, such as temperature.
The prompt, the reply and 32 tokens of chat-template overhead must fit in the model's context window — 4,096 tokens for Qwen/Qwen2.5-7B-Instruct on this deployment, as reported by GET /v1/models. If fewer than 64 tokens would be left for the reply, the request fails with context_length_exceeded.
Request headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer followed by your API key. |
Content-Type |
Yes | application/json. |
X-Mortar-Session |
No | Names the conversation within your organization: up to 128 letters, digits, -, _, . or :, and any other character is dropped. Requests in one session are routed to the replica that already holds their prompt prefix. Defaults to user; with neither, the request starts a session of its own. |
X-Mortar-Optimize |
No | off processes the prompt exactly as sent. Opt-in values include json-tool-blocks for JSON-fenced data-tool results and collapse-spaces for aligned tool output. on asks for the lossless defaults only, overriding an organization default of off. Any unknown value is refused with 400. The deployment's mode still applies: in shadow mode, the default, nothing is changed, only measured, and when it is off no value turns optimization on. Keep the value the same for a whole conversation. |
X-Mortar-Tool-Select |
No | named keeps only the function explicitly named by object-form tool_choice. This is an opt-in progressive-disclosure fast path for clients that send a full catalogue even though they have already selected one function. Other values, or named without a named choice, are refused with 400. |
X-Mortar-Dedupe |
No | in-flight coalesces concurrently executing, byte-identical non-streaming requests within the same tenant. It requires stream: false and an Idempotency-Key. A key reused concurrently with a different request is refused with 409; no completed response is cached. |
Idempotency-Key |
With in-flight deduplication | A 1–128 character operation key using letters, digits, ., _, - or :. It is scoped to the authenticated tenant and combined with the exact request and optimization/session headers. |
X-Request-Id |
No | Your own ID for the request, echoed back in the response. |
Response headers
| Header | Description |
|---|---|
X-Mortar-Request-Id |
The completion's ID, the same as id in the body. Pass it to stop the generation. |
X-Mortar-Deployment |
The deployment that served the request. |
X-Mortar-Model |
The model that generated the reply. |
X-Mortar-Upstream |
Present when the deployment forwards requests to a model server, and names that server. |
X-Mortar-Session |
The session the request ran in: the one you named, or the one it started. Follow it on the telemetry stream. |
X-Mortar-Optimizer |
What the prompt optimizer did: a report such as ruleset=2; mode=on; bytes=541; saved=77; passes=json-compact/1,json-tool-blocks/1; elapsed_us=212, where elapsed_us is how long optimization took in microseconds, or ruleset=2; mode=off while it's off. In shadow mode, the default, the prompt was used as sent and saved is what optimization would have removed. |
X-Mortar-Tool-Selector |
Present when named tool selection ran, for example mode=named; tools=20->1; tokens=2860->144; saved=2716. Counts use the same tokenizer as prompt planning. |
X-Mortar-Dedupe |
leader for the request that reached inference, or shared for a concurrent request that reused that exact in-flight result. Shared responses intentionally carry the leader's completion ID. |
X-Request-Id |
Your X-Request-Id, or one Mortar generated. |
Example
curl https://api.mortar.one/v1/chat/completions \
-H "Authorization: Bearer $MORTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-7B-Instruct",
"messages": [
{"role": "system", "content": "You are a concise assistant for an inference platform."},
{"role": "user", "content": "What does max_tokens limit?"}
],
"max_tokens": 200,
"stream": false
}'{
"id": "chatcmpl-9b2f4d6e8a0c4b1d3f5e7a9c",
"object": "chat.completion",
"created": 1767225600,
"model": "Qwen/Qwen2.5-7B-Instruct",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "It caps how many tokens the model may generate for this reply."
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 32,
"completion_tokens": 13,
"total_tokens": 45
}
}The chat completion object
| Field | Type | Description |
|---|---|---|
id |
string | chatcmpl- followed by 24 hexadecimal characters. |
object |
string | Always chat.completion. |
created |
integer | When the request was accepted, in Unix seconds. |
model |
string | The model that generated the reply. |
choices |
array | Always exactly one choice. |
choices[].index |
integer | Always 0. |
choices[].message.role |
string | Always assistant. |
choices[].message.content |
string or null | The reply, or null when the reply only calls tools. |
choices[].message.tool_calls |
array | Present when the model calls tools: the calls, in order. |
choices[].message.tool_calls[].id |
string | The call's ID, to repeat in the tool_call_id of the message that answers it. |
choices[].message.tool_calls[].type |
string | Always function. |
choices[].message.tool_calls[].function.name |
string | The function to call. |
choices[].message.tool_calls[].function.arguments |
string | Its arguments, as JSON text. Parse them before you trust them: a model can produce invalid JSON. |
choices[].logprobs |
null | Always null. Log probabilities aren't returned. |
choices[].finish_reason |
string | stop when the reply is complete, tool_calls when it calls tools, and length when it was cut off by max_tokens or the context window. When the deployment forwards to a model server, that server's own reason is passed through. A server that ignores max_tokens is stopped by Mortar at three times it plus 64 tokens, by Mortar's estimate, with length. |
usage.prompt_tokens |
integer | Tokens in the prompt, including chat-template framing. |
usage.completion_tokens |
integer | Tokens in the reply. |
usage.total_tokens |
integer | The sum of the two. |
When the deployment forwards to a model server that reports usage, these are that server's own counts, and the prompt_tokens_details and completion_tokens_details it sends, such as cached_tokens, are passed through unchanged. Otherwise, and for a reply Mortar ended itself because the model server ran on past max_tokens, they're Mortar's own estimate, the same one its context-window check uses. Either way prompt_tokens counts the prompt after the prompt optimizer, which is the prompt the model reads, and the telemetry stream says in usageSource which kind of count the request was billed on.
Streaming
With stream true — or left out — the response is a text/event-stream. Each event is one data: line holding a chat.completion.chunk, followed by a blank line, and the stream ends with data: [DONE].
This request leaves stream out, so the reply streams:
curl -N https://api.mortar.one/v1/chat/completions \
-H "Authorization: Bearer $MORTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "What does max_tokens limit?"}],
"max_tokens": 200
}'data: {"id":"chatcmpl-1e3a5c7b9d0f2a4c6e8b0d1f","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"delta":{"role":"assistant"},"logprobs":null,"finish_reason":null}]}
data: {"id":"chatcmpl-1e3a5c7b9d0f2a4c6e8b0d1f","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"delta":{"content":"It caps how"},"logprobs":null,"finish_reason":null}]}
data: {"id":"chatcmpl-1e3a5c7b9d0f2a4c6e8b0d1f","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"logprobs":null,"finish_reason":"stop"}]}
data: [DONE]The chunks arrive in this order:
- One chunk whose
deltaholdsrole: "assistant"and no text. - One chunk per piece of the reply, each with the next text in
delta.content. - One chunk with
finish_reasonset and nodelta. - With
stream_options.include_usage, one chunk with an emptychoicesarray and ausageobject. data: [DONE].
The chunk object
| Field | Type | Description |
|---|---|---|
id |
string | The completion's ID, the same on every chunk. |
object |
string | Always chat.completion.chunk. |
created |
integer | When the request was accepted, in Unix seconds. |
model |
string | The model generating the reply. |
choices |
array | One choice, or none on the usage chunk. |
choices[].index |
integer | Always 0. |
choices[].delta.role |
string | assistant, on the first chunk only. |
choices[].delta.content |
string | The next piece of the reply. |
choices[].delta.tool_calls |
array | The next piece of the tool calls, when the model calls tools. |
choices[].delta.tool_calls[].index |
integer | Which call the piece belongs to, counting from 0. Pieces of parallel calls can interleave. |
choices[].delta.tool_calls[].id |
string | The call's ID, in its first piece. |
choices[].delta.tool_calls[].type |
string | function, in its first piece. |
choices[].delta.tool_calls[].function.name |
string | The function, in its first piece. |
choices[].delta.tool_calls[].function.arguments |
string | The next piece of the arguments. Join a call's pieces in order to get its JSON. |
choices[].logprobs |
null | Always null. |
choices[].finish_reason |
string or null | null on every chunk except the finish chunk, which carries stop, tool_calls or length. |
usage |
object | Only on the usage chunk: prompt_tokens, completion_tokens and total_tokens, counted as for a chat completion. |
Usage in a stream
Ask for usage and Mortar adds one chunk after the finish chunk:
curl -N https://api.mortar.one/v1/chat/completions \
-H "Authorization: Bearer $MORTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "What does max_tokens limit?"}],
"max_tokens": 200,
"stream": true,
"stream_options": {"include_usage": true}
}'data: {"id":"chatcmpl-7c9e1a3b5d0f2c4e6a8b0d2f","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"logprobs":null,"finish_reason":"stop"}]}
data: {"id":"chatcmpl-7c9e1a3b5d0f2c4e6a8b0d2f","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[],"usage":{"prompt_tokens":17,"completion_tokens":13,"total_tokens":30}}
data: [DONE]When a stream ends early
- A model-server error mid-stream arrives as one event holding the error envelope with
typeset toupstream_error, and the stream closes without[DONE]. A stream that closes without[DONE]is incomplete; treat it as failed. - A stopped generation sends no finish chunk. The usage chunk, if you asked for it, and
[DONE]still follow. - Keep-alive comments. When the deployment forwards to a model server, Mortar sends a
: pingcomment line every 10 seconds so proxies don't close a quiet connection. Event-stream parsers ignore comments.
The streaming guide shows how to consume the stream in each SDK.
Errors
| Status | Code | When |
|---|---|---|
| 400 | The body isn't valid JSON; messages is missing, empty or longer than 512 entries; a message lacks a string role or a string-or-array content; a tool call, a tool message's tool_call_id, tools, tool_choice or parallel_tool_calls isn't shaped as described above; max_tokens isn't a positive whole number; or X-Mortar-Optimize has a value Mortar doesn't recognise. |
|
| 400 | context_length_exceeded |
Fewer than 64 tokens of the context window are left for the reply. Start a new conversation or send fewer messages. |
| 404 | model_not_found |
model isn't in the catalog. |
| 503 | model_not_available |
model is in the catalog but not deployed yet. |
| 502, 503, 504 | upstream_error, upstream_unavailable, upstream_timeout |
The deployment forwards to a model server, and that server failed, was unreachable or timed out. |
curl https://api.mortar.one/v1/chat/completions \
-H "Authorization: Bearer $MORTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}], "stream": false}'{
"error": {
"message": "The model `gpt-4o` does not exist.",
"type": "invalid_request_error",
"code": "model_not_found"
}
}The API overview lists the errors every endpoint can return.
Call tools
Describe functions in tools and the model can answer with calls to them instead of text. The reply then has finish_reason tool_calls, null content, and the calls in message.tool_calls. Run them, and send the conversation back with that assistant message as you received it, followed by one tool message for each call: its tool_call_id names the call and its content holds what the function returned. The model reads the results and answers, or calls again.
curl https://api.mortar.one/v1/chat/completions \
-H "Authorization: Bearer $MORTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"stream": false
}'{
"id": "chatcmpl-4d6f8a0c2e4b6d8f0a2c4e6b",
"object": "chat.completion",
"created": 1767225600,
"model": "Qwen/Qwen2.5-7B-Instruct",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_8e1f3a5c7b9d2e4f6a8c0b1d",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}
}
]
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 61,
"completion_tokens": 9,
"total_tokens": 70
}
}Send the result back with the call that asked for it:
curl https://api.mortar.one/v1/chat/completions \
-H "Authorization: Bearer $MORTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "What is the weather in Paris?"},
{"role": "assistant", "content": null, "tool_calls": [{
"id": "call_8e1f3a5c7b9d2e4f6a8c0b1d",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}
}]},
{"role": "tool", "tool_call_id": "call_8e1f3a5c7b9d2e4f6a8c0b1d", "content": "{\"temp_c\": 21, \"sky\": \"clear\"}"}
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"stream": false
}'{
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "It's 21 °C and clear in Paris."},
"logprobs": null,
"finish_reason": "stop"
}
]
}Streamed, each call arrives in pieces. The first piece of a call carries its index, id, type and function name; the pieces after it carry only the index and the next part of the arguments. Join each call's arguments in order. With parallel_tool_calls, the pieces of several calls can interleave, and index tells them apart.
curl -N https://api.mortar.one/v1/chat/completions \
-H "Authorization: Bearer $MORTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
}
}],
"tool_choice": {"type": "function", "function": {"name": "get_weather"}}
}'data: {"id":"chatcmpl-6b8d0f2a4c6e8a0b2d4f6a8c","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"delta":{"role":"assistant"},"logprobs":null,"finish_reason":null}]}
data: {"id":"chatcmpl-6b8d0f2a4c6e8a0b2d4f6a8c","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_2b4d6f8a0c1e3a5c7e9b1d3f","type":"function","function":{"name":"get_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]}
data: {"id":"chatcmpl-6b8d0f2a4c6e8a0b2d4f6a8c","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":\"Paris\"}"}}]},"logprobs":null,"finish_reason":null}]}
data: {"id":"chatcmpl-6b8d0f2a4c6e8a0b2d4f6a8c","object":"chat.completion.chunk","created":1767225600,"model":"Qwen/Qwen2.5-7B-Instruct","choices":[{"index":0,"logprobs":null,"finish_reason":"tool_calls"}]}
data: [DONE]When the deployment forwards to a model server, the tools, tool_choice and parallel_tool_calls go to it as you sent them, and its calls come back as it made them, streamed pieces included. With no model server, Mortar's scripted model calls tools itself, so an agent loop can be tested without one. It calls the function tool_choice names, or every tool for required, or only the first when parallel_tool_calls is false. Otherwise it calls the first tool, until the conversation ends with tool results, which it answers in text. It fills each required argument with an empty value of its type.
Stop a generation
/v1/generations/{id}/stop
Stops a chat completion while it is still being generated. Closing the connection stops a generation too; use this endpoint when the request that is generating isn't yours to close, such as a cancel button handled by another server. Needs an API key from the organization that started the generation.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id |
string | The completion's ID, from id in any chunk or from the X-Mortar-Request-Id header. |
Example
curl -X POST https://api.mortar.one/v1/generations/chatcmpl-1e3a5c7b9d0f2a4c6e8b0d1f/stop \
-H "Authorization: Bearer $MORTAR_API_KEY"{
"stopped": true,
"id": "chatcmpl-1e3a5c7b9d0f2a4c6e8b0d1f"
}Response
| Field | Type | Description |
|---|---|---|
stopped |
boolean | true if a running generation was stopped. |
id |
string | The ID you sent. |
The status is 200 when a generation was stopped and 404 when none with that ID is running for your organization: it already finished, the ID is wrong, or another organization started it. Mortar answers all three the same way, so the endpoint can't be used to find out which IDs are live. Both statuses carry the same body; the 404 doesn't use the error envelope.