Chat completions¶
POST /v1/chat/completions, OpenAI chat-completions schema, streaming and non-streaming.
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) |
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 |
reasoning_effort |
the reasoning switch, see 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) |
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 (route-around) |
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 explicit400 reasoning_not_disableable.- Migrating code that uses another vendor's spelling?
enable_thinking,thinking: {"type": "disabled"},chat_template_kwargs: {"enable_thinking": false}, and the OpenRouter/Vercel object,reasoning: {"enabled": …},reasoning: {"effort": …}, orreasoning: {"max_tokens": …}(a thinking budget implies on; the budget is honored where the serving route has a native budget knob), are all accepted and mean the same thing.
The thinking trace comes back in one canonical field plus an OpenRouter/Vercel-compatible mirror, whatever capacity served you:
"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.
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 assystem),user,assistant,tool. contentcan be a string or an array of text parts, both work identically.cache_controlfields 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.
Usage block (every response)¶
"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, one canonical field on every route.cost, this response's exact spend in USD at the published rate card: the same inputs and the same rates billing records. Present on every priced response, OpenRouter'susage: {"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,costis predictable, not a floating market quote.completion_tokensincludes reasoning tokens (they are generated output). On reasoning responses the split is itemized ascompletion_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¶
- Streams are made whole. Hedging and mid-stream failover happen out of sight, you get one continuous SSE stream either way (routing & failover).
- Responses never identify a serving route.
modelis always the Keln model id; the trace field, cache field, and error shapes are identical everywhere. - Oversized requests fail fast. A prompt that can't fit the model's context window
returns
400 context_length_exceeded, before any tokens are billed. - Big bodies are fine. 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
:nitroand:floorare accepted and stripped, the router already optimizes harder than the hint asks.:freeand:onlinereturn a400naming 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).
Errors¶
Stable JSON error envelope + status semantics: Errors & limits.