# Chat completions
`POST /v1/chat/completions`, OpenAI chat-completions schema, streaming and non-streaming.
```bash
curl https://api.keln.ai/v1/chat/completions \
-H "Authorization: Bearer $KELN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-ai/deepseek-v4-flash",
"messages": [{"role": "user", "content": "Hello"}],
"stream": true
}'
```
## Parameters
Send standard parameters; the contract for each:
| Parameters | Contract |
|---|---|
| `model`, `messages` | required. `content` may be a string or a content-parts array; models with the **Vision** badge accept `image_url` parts ([Images](../capabilities/images.md)) |
| `temperature`, `top_p` | passed through to the serving engine |
| `max_tokens` / `max_completion_tokens` | output cap, either name works (`max_completion_tokens` wins if both). The cap covers reasoning + answer together. |
| `stream`, `stream_options.include_usage` | standard OpenAI semantics, [streaming](streaming.md) |
| `reasoning_effort` | the reasoning switch, see [Reasoning](#reasoning) |
| `response_format` (`json_object` / `json_schema`) | honored and validated, differently by transport. Non-streaming: the output is checked before it is relayed; malformed output retries on another route first. Streaming: your response relays live, token by token, with validation running as the stream flows, a route that emits defective structure is penalized and de-routed, so a defect can reach you at most transiently ([details](../capabilities/structured-outputs.md)) |
| `tools`, `tool_choice` (incl. `"required"` / named function) | honored, with the same validation split applied to tool-call arguments. Streamed tool-call fragment indices are repaired in flight to dense 0-based numbering, so SDKs that accumulate fragments by index work unchanged |
| `stop`, `seed`, `n`, `logprobs`, `top_logprobs`, `logit_bias`, `frequency_penalty`, `presence_penalty`, `parallel_tool_calls` | honored where capacity supports them, routing prefers capacity that does ([normalization](../capabilities/normalization.md)) |
| `n` | at most 8 completions per request (`400 n_too_large` above it); every completion is generated and billed |
| `top_k`, `min_p`, `repetition_penalty`, `top_a` | open-model extensions, accepted and passed through where the serving engine honors them |
| `user`, `safety_identifier` | accepted and stripped, end-user identifiers are never forwarded upstream ([ZDR](../capabilities/privacy.md)) |
| anything else | accepted and ignored, an unknown key can never fail your request |
## Reasoning
Most catalog models are hybrid-reasoning models: they can think before answering. Control it
with one standard field:
```json
{ "reasoning_effort": "none" } // answer directly, no thinking, lower latency & cost
{ "reasoning_effort": "high" } // think first (low / medium / high / max all enable thinking)
```
- **Omitted** → thinking **on at the model's deepest setting**, on every thinking-capable model
(including ones whose upstream default is off). Levels ask for less; only an explicit
`"none"` turns thinking off. Thinking bills as output tokens, so cost- or latency-sensitive
traffic should send `"none"` explicitly, [why](../capabilities/reasoning.md#the-default-is-full-depth).
- **`"max"`** → the deepest setting the serving route provably honors (`"xhigh"` is an alias).
- **`"none"`** → reasoning off. Keln translates this to whatever the serving route requires,
you send one field, it works everywhere. If a model cannot disable reasoning, you get an
explicit `400 reasoning_not_disableable`.
- Keln accepts other vendors' spellings and treats them the same: `enable_thinking`,
`thinking: {"type": "disabled"}`, `chat_template_kwargs: {"enable_thinking": false}`, and the
OpenRouter/Vercel object, `reasoning: {"enabled": …}`, `reasoning: {"effort": …}`, or
`reasoning: {"max_tokens": …}` (a thinking budget implies **on**; the budget is honored where
the serving route supports a thinking budget).
The thinking trace comes back in one canonical field plus an OpenRouter/Vercel-compatible
mirror, whatever capacity served you:
```json
"message": { "content": "The answer is 391.", "reasoning_content": "17 × 23 … ", "reasoning": "17 × 23 … " }
```
`reasoning_content` is canonical; `reasoning` always carries the identical trace, so code written
against either gateway's response shape reads it unchanged.
Streaming: `delta.reasoning_content`, mirrored in `delta.reasoning`. Reasoning tokens are billed
as output tokens and included in `usage.completion_tokens`. Full guide:
[Reasoning control](../capabilities/reasoning.md).
!!! tip "Multi-turn tool calling"
When you replay conversation history that includes tool calls, include the assistant
messages' `reasoning_content` exactly as you received it, these models use it to continue
the chain. History that carries the trace only in `reasoning` (saved OpenRouter/Vercel
responses) works too: it folds into the canonical field before dispatch.
## Messages
- Roles: `system`, `developer` (treated as `system`), `user`, `assistant`, `tool`.
- `content` can be a string or an array of text parts, both work identically.
- `cache_control` fields from other providers' conventions are ignored
([caching is automatic](../capabilities/caching.md)).
## Keln-specific response header
If your request carried fields Keln accepts but deliberately doesn't act on (`provider`, `models`,
`transforms`, …), they're named in `x-keln-inert-params`. The
[migration guide](../migrate.md#every-openrouter-field-accounted-for) explains each field's
behavior.
## Usage block (every response)
```json
"usage": {
"prompt_tokens": 1874,
"completion_tokens": 212,
"total_tokens": 2086,
"prompt_tokens_details": { "cached_tokens": 1792 },
"cost": 0.00013356
}
```
- `cached_tokens`, prompt tokens billed at the [per-model cached rate](../capabilities/caching.md),
one canonical field on every route.
- `cost`, this response's exact spend in USD at the published
[Models & pricing](../models-pricing.md) rates: the same inputs and the same rates billing records. Present on every priced response, OpenRouter's
`usage: {"include": true}` request flag is accepted and unnecessary. On a stream it arrives in
the final usage chunk, so ask for that chunk with
`"stream_options": {"include_usage": true}`. Because prices are fixed, `cost` is predictable.
- `completion_tokens` includes reasoning tokens (they are generated output). On reasoning
responses the split is itemized as `completion_tokens_details.reasoning_tokens`: where the
serving route reports a reasoning split, it is passed through as reported; where it reports
none, Keln derives the split with the model's own tokenizer and includes it only when the
arithmetic reconciles with the billed total
([details](../capabilities/reasoning.md#reasoning_tokens-in-usage)).
- Streaming: the usage chunk arrives at stream end when you set
`"stream_options": {"include_usage": true}`, [streaming](streaming.md).
## Behavior notes
- **One continuous stream.** Hedging and mid-stream failover happen without interrupting it; you
get one continuous SSE stream either way ([routing & failover](../capabilities/routing.md)).
- **Responses never identify a serving route.** `model` is always the Keln model id; the trace
field, cache field, and error shapes are identical everywhere.
- **Oversized requests fail before billing.** A prompt that can't fit the model's context window
returns `400 context_length_exceeded`, before any tokens are billed.
- **Request body limit.** Request bodies up to 32 MB are accepted, enough to send a
full-window prompt on a 1M-context model. A larger body is rejected with `413`.
- **Model-id suffixes.** OpenRouter-style `:nitro` and `:floor` are accepted and stripped, the
router already optimizes harder than the hint asks. `:free` and `:online` return a `400` naming
the suffix.
- **Billing = engine-reported usage** on the response you actually received; Keln-side failures
that produced nothing bill nothing.
- **Canceling a request bills its input** plus any output already delivered, the serving engine
ran that prefill whether or not you read the answer. Keln-side errors and timeouts remain free.
- Requests and responses are never stored ([ZDR](../capabilities/privacy.md)).
## Errors
Stable JSON error envelope + status semantics: [Errors & limits](errors.md).
Chat completions
POST /v1/chat/completions, OpenAI chat-completions schema, streaming and non-streaming.
honored and validated, differently by transport. Non-streaming: the output is checked before it is relayed; malformed output retries on another route first. Streaming: your response relays live, token by token, with validation running as the stream flows, a route that emits defective structure is penalized and de-routed, so a defect can reach you at most transiently (details)
tools, tool_choice (incl. "required" / named function)
honored, with the same validation split applied to tool-call arguments. Streamed tool-call fragment indices are repaired in flight to dense 0-based numbering, so SDKs that accumulate fragments by index work unchanged
stop, seed, n, logprobs, top_logprobs, logit_bias, frequency_penalty, presence_penalty, parallel_tool_calls
honored where capacity supports them, routing prefers capacity that does (normalization)
n
at most 8 completions per request (400 n_too_large above it); every completion is generated and billed
top_k, min_p, repetition_penalty, top_a
open-model extensions, accepted and passed through where the serving engine honors them
user, safety_identifier
accepted and stripped, end-user identifiers are never forwarded upstream (ZDR)
anything else
accepted and ignored, an unknown key can never fail your request
Reasoning
Most catalog models are hybrid-reasoning models: they can think before answering. Control it
with one standard field:
{"reasoning_effort":"none"}// answer directly, no thinking, lower latency & cost{"reasoning_effort":"high"}// think first (low / medium / high / max all enable thinking)
Omitted → thinking on at the model's deepest setting, on every thinking-capable model
(including ones whose upstream default is off). Levels ask for less; only an explicit
"none" turns thinking off. Thinking bills as output tokens, so cost- or latency-sensitive
traffic should send "none" explicitly, why.
"max" → the deepest setting the serving route provably honors ("xhigh" is an alias).
"none" → reasoning off. Keln translates this to whatever the serving route requires,
you send one field, it works everywhere. If a model cannot disable reasoning, you get an
explicit 400 reasoning_not_disableable.
Keln accepts other vendors' spellings and treats them the same: enable_thinking,
thinking: {"type": "disabled"}, chat_template_kwargs: {"enable_thinking": false}, and the
OpenRouter/Vercel object, reasoning: {"enabled": …}, reasoning: {"effort": …}, or
reasoning: {"max_tokens": …} (a thinking budget implies on; the budget is honored where
the serving route supports a thinking budget).
The thinking trace comes back in one canonical field plus an OpenRouter/Vercel-compatible
mirror, whatever capacity served you:
reasoning_content is canonical; reasoning always carries the identical trace, so code written
against either gateway's response shape reads it unchanged.
Streaming: delta.reasoning_content, mirrored in delta.reasoning. Reasoning tokens are billed
as output tokens and included in usage.completion_tokens. Full guide:
Reasoning control.
Multi-turn tool calling
When you replay conversation history that includes tool calls, include the assistant
messages' reasoning_content exactly as you received it, these models use it to continue
the chain. History that carries the trace only in reasoning (saved OpenRouter/Vercel
responses) works too: it folds into the canonical field before dispatch.
Messages
Roles: system, developer (treated as system), user, assistant, tool.
content can be a string or an array of text parts, both work identically.
cache_control fields from other providers' conventions are ignored
(caching is automatic).
Keln-specific response header
If your request carried fields Keln accepts but deliberately doesn't act on (provider, models,
transforms, …), they're named in x-keln-inert-params. The
migration guide explains each field's
behavior.
cached_tokens, prompt tokens billed at the per-model cached rate,
one canonical field on every route.
cost, this response's exact spend in USD at the published
Models & pricing rates: the same inputs and the same rates billing records. Present on every priced response, OpenRouter's
usage: {"include": true} request flag is accepted and unnecessary. On a stream it arrives in
the final usage chunk, so ask for that chunk with
"stream_options": {"include_usage": true}. Because prices are fixed, cost is predictable.
completion_tokens includes reasoning tokens (they are generated output). On reasoning
responses the split is itemized as completion_tokens_details.reasoning_tokens: where the
serving route reports a reasoning split, it is passed through as reported; where it reports
none, Keln derives the split with the model's own tokenizer and includes it only when the
arithmetic reconciles with the billed total
(details).
Streaming: the usage chunk arrives at stream end when you set
"stream_options": {"include_usage": true}, streaming.
Behavior notes
One continuous stream. Hedging and mid-stream failover happen without interrupting it; you
get one continuous SSE stream either way (routing & failover).
Responses never identify a serving route.model is always the Keln model id; the trace
field, cache field, and error shapes are identical everywhere.
Oversized requests fail before billing. A prompt that can't fit the model's context window
returns 400 context_length_exceeded, before any tokens are billed.
Request body limit. Request bodies up to 32 MB are accepted, enough to send a
full-window prompt on a 1M-context model. A larger body is rejected with 413.
Model-id suffixes. OpenRouter-style :nitro and :floor are accepted and stripped, the
router already optimizes harder than the hint asks. :free and :online return a 400 naming
the suffix.
Billing = engine-reported usage on the response you actually received; Keln-side failures
that produced nothing bill nothing.
Canceling a request bills its input plus any output already delivered, the serving engine
ran that prefill whether or not you read the answer. Keln-side errors and timeouts remain free.