OpenAI parameter compatibility
Which chat-completions request parameters are implemented, rejected, or silently tolerated — and the response fields every reply carries.
POST /v1/chat/completions targets OpenAI drop-in compatibility: an existing
SDK or proxy should keep working unmodified. That does not mean every field is
implemented. The engine sorts every request parameter into one of three
regimes (internal/features/chat/inbound/dto.go), and this page is the
generated-by-hand matrix behind that policy (spec 2026-08-11 DX audit
§4.5/§12).
The three regimes
- Implemented — present in the request DTO, transmitted to the gateway, and changes the response.
- Rejected (
400) — not implemented, and the engine's own test says its absence would change what the caller receives or pays for. Refusing is the honest floor: an ignored field here would silently return a cheaper or different answer than the one asked for. - Tolerated — not implemented, but ignoring it changes nothing the caller can observe. Kept ignored on purpose: rejecting every field the engine does not model would break drop-in compatibility on the next field upstream ships.
The test for regime 2 vs. regime 3 is never "do we model it" — it is "does
ignoring it change the answer." That is also why this is a short, explicit
list rather than DisallowUnknownFields on the request body.
Implemented
| Parameter | Behavior |
|---|---|
model, messages, stream | Core request shape. |
temperature, top_p | Sampling controls, forwarded to the gateway; both dialects reject out-of-range values (temperature outside 0..2, top_p outside 0..1) with 400 before calling a provider. |
max_tokens | Forwarded as the completion cap. ⚠️ Honored by the connector's provider type — see the API surface and the connector documentation for per-type caveats. |
max_completion_tokens | Upstream's replacement for the deprecated max_tokens; its own field, not an alias, because it bounds visible output and reasoning tokens together. Either field alone is honored; both together are accepted only when equal — differing values answer 400 naming both fields, so a spend cap can never silently disappear. |
stop | Forwarded verbatim; a single string or an array of strings. |
store | Controls conversation persistence: absent or true persists the turn, false opts it out. |
tools, tool_choice, response_format | Tool-calling (streamed and non-streamed) and structured output (json_object / json_schema), forwarded to the provider; rejected in knowledge-agent mode, which owns its own answer shape. |
prompt_cache_key, prompt_cache_retention | Forwarded verbatim — they choose which cache a turn reuses, an isolation decision only the caller can make. The gateway strips them for providers that do not support them. |
reasoning_effort | none | minimal | low | medium | high | xhigh (the union of provider-accepted values). Tells a reasoning model how hard to think; none is what makes a grounded RAG answer fast and cheap. |
stream_options.include_usage | When true on a streamed request, one final chat.completion.chunk carries a usage object and an empty choices array after the finish-reason chunk, so a streamed call can be metered like a non-streamed one. Present on a non-streaming request answers 400 — OpenAI defines the field as meaningless there. |
dataset_ids, project_id, conversation_id, agent_mode | Deeplinq extensions: RAG, projects, conversations, and the governed knowledge agent. |
Rejected (400, regime 1)
Each of these fails the same test n and best_of always applied: without
it being implemented, silently ignoring an explicit ask changes what the
caller gets back or pays for. Sending the parameter's own OpenAI-documented
default value is not a real ask — it is asking for what the engine
already does — so it passes rather than refuses, mirroring the reasoning the
n <= 1 guard has always used. An explicit JSON null on a nullable field is
treated as absent, not a real ask.
| Parameter | Rejected because | Tolerated (no-op) value |
|---|---|---|
n | The engine always returns exactly one choice; a caller asking for more was silently billed for one anyway (audit L1). | n <= 1 |
best_of | The engine always generates exactly one candidate. | best_of <= 1 |
logprobs | The response never carries token log probabilities. | false (OpenAI default) |
top_logprobs | Meaningless — and refused by OpenAI itself — without an accompanying logprobs: true; requested probabilities are never returned. | Any value, as long as the sibling logprobs is not itself true (OpenAI ties the two together; there is no independent default for top_logprobs) |
presence_penalty | Sampling is not adjusted for token presence. | 0 (OpenAI default) |
frequency_penalty | Sampling is not adjusted for token frequency. | 0 (OpenAI default) |
parallel_tool_calls | The engine's own tool-call concurrency is always used; false cannot be honored. | true (OpenAI default) |
seed | Responses are not reproducible by seed. | None — OpenAI documents no default; any explicit, non-null seed is a genuine ask. |
logit_bias | Requested per-token biases are not applied. | {} (empty — equivalent to OpenAI's default of no bias) |
Tolerated (regime 3, unmodeled)
| Parameter | Why it stays ignored |
|---|---|
user | Upstream per-end-user attribution. Superseded, not merely unimplemented: the engine already carries its own caller identity (the token's auth.Principal) on every request. |
service_tier, metadata | No observable effect on the response or the bill. |
| Any unrecognized or future OpenAI field | Rejecting unknown fields would break every SDK and proxy the moment upstream ships something new — the deliberate cost of staying drop-in. A typo such as tempreature is accepted and silently ignored for the same reason; it is an accepted trade-off, not an oversight. |
Response contract
Every response — streamed or not — carries the full OpenAI envelope shape:
| Field | Non-streamed | Streamed (every chunk) |
|---|---|---|
id, object, model | ✔ | ✔ |
created | ✔ | ✔ |
choices | ✔ | ✔ ([] on the trailing usage chunk, when present) |
usage | ✔ | Only on the trailing chunk, when stream_options.include_usage was true |
All chunks belonging to one response share the same id, created, and
model. model reports the concrete served model on the non-streamed path;
on a streamed response it echoes the requested value (which may be "auto"),
since the concrete model is not known chunk-by-chunk.
The Anthropic dialect (POST /v1/messages) has its own, already-conforming
contract and is not covered by this page.