Skip to content

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 explicit 400 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": …}, or reasoning: {"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 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.

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'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, not a floating market quote.
  • 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

  • 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. model is 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 :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).

Errors

Stable JSON error envelope + status semantics: Errors & limits.