
# Request parameters

LowRouter routes one OpenAI-shaped request to very different providers.
They don't all support the same parameters. This page covers what
happens to each parameter on each kind of route. It also covers how to
tell from the response whether a parameter was applied.

## Nothing is dropped silently

Every parameter you send ends in one of three ways:

- **Applied.** It is translated onto the provider's own control and has
  its effect.
- **Rejected with a 400.** This happens when the parameter would change
  the *answer* and the route cannot express it. Examples: asking for
  JSON and getting prose, asking for log probabilities and getting none.
  The error names the parameter and the provider:

  ```json
  {
    "error": {
      "type": "invalid_request_error",
      "message": "LowRouter rejected the request before it reached provider anthropic: logprobs is not supported by provider anthropic: this route does not return token log probabilities",
      "param": "logprobs"
    }
  }
  ```

  On an `auto/` request this does not end the request. Other routes may
  support the parameter, so LowRouter tries the next route. The 400
  only comes back when no route can apply it.
- **Disclosed.** This happens when the parameter is *advisory*, such as
  a reasoning level, a seed or a sampling tweak. Leaving it out still
  gives a correct answer, so the request goes ahead and the response
  lists the parameter in `lowrouter_unapplied`.

## `lowrouter_unapplied`

`lowrouter_unapplied` appears on a completion only when something was
not applied. If every parameter was applied, the field is absent.
Here's an example:

```json
"lowrouter_unapplied": [
  {
    "parameter": "seed",
    "reason": "unsupported_on_this_path",
    "detail": "provider anthropic has no equivalent control"
  },
  {
    "parameter": "made_up_param_xyz",
    "reason": "unrecognized_parameter",
    "detail": "LowRouter does not recognise this parameter, so no provider received it"
  }
]
```

On a stream, it's on the final chunk next to `lowrouter_metadata`.
Match on `parameter` and `reason` in your code. `detail` is
human-readable prose and may change.

| `reason` | Meaning |
|---|---|
| `unsupported_on_this_path` | The serving provider has no equivalent control. |
| `rejected_by_provider` | The provider refused the parameter for this model. The request was retried without it. |
| `conflicts_with_sampling` | Extended thinking was not enabled because it conflicts with your sampling settings. Anthropic requires `temperature` 1, no `top_k`, and `top_p` of at least 0.95. A sampling parameter the model refused, and that was therefore removed, does not count. |
| `max_tokens_too_small` | Your `max_tokens` leaves no room for a thinking budget. |
| `unrecognized_parameter` | LowRouter does not know this top-level key, so no provider received it. |

### Unknown parameters

A top-level key LowRouter does not model is **accepted and disclosed**.
LowRouter does not reject it: many clients send another gateway's
extensions, such as OpenRouter's `provider` or Vercel's
`providerOptions`, and refusing those would break the client. An
unknown key is never forwarded to a provider, though. The
`unrecognized_parameter` entry tells you it had no effect.

## Parameter support

The columns are the four kinds of route. The first, OpenAI-compatible,
covers OpenAI, Mistral, Scaleway, OVHcloud, Vertex model-as-a-service
and the other providers that speak OpenAI's format.

| Parameter | OpenAI-compatible | Anthropic (incl. Claude on Bedrock/Vertex) | Gemini | Bedrock (other models) |
|---|---|---|---|---|
| `response_format` `json_schema` | forwarded | applied | applied | applied |
| `response_format` `json_object` | forwarded | **400** | applied | **400** |
| `logprobs`, `top_logprobs` | forwarded | **400** | **400** | **400** |
| `logit_bias` | forwarded | **400** | **400** | **400** |
| `n` > 1 | forwarded | **400** | applied | **400** |
| `reasoning_effort` | forwarded | applied (extended thinking) | applied (thinking) | disclosed |
| `seed` | forwarded (as `random_seed` on Mistral) | disclosed | applied | disclosed |
| `temperature`, `top_p` | forwarded | applied; disclosed (removed) on models that refuse them | applied | applied |
| `top_k` | forwarded; disclosed (removed) on OVHcloud | applied; disclosed (removed) on models that refuse it | applied | disclosed |
| `presence_penalty`, `frequency_penalty` | forwarded | disclosed | applied | disclosed |
| `repetition_penalty`, `metadata`, `prediction` | forwarded; disclosed (removed) on OVHcloud | disclosed | disclosed | disclosed |
| `service_tier` | `auto` and `default` forwarded; any other tier disclosed (removed). Always removed on OVHcloud | disclosed | disclosed | disclosed |
| `user` | forwarded; disclosed (removed) on Mistral and OVHcloud | applied | disclosed | disclosed |
| `name` on a message (disclosed as `messages.name`) | forwarded; disclosed (removed) on Mistral | disclosed | disclosed | disclosed |
| `cache_control` (content-part marker) | disclosed (removed) | applied | disclosed (removed) | applied |
| `tool_result` carrying an image or document | disclosed (moved to a user turn) | applied | disclosed (moved to a user turn) | applied |
| `stream_options.include_usage` | accepted; the usage frame is always sent | same | same | same |

**Forwarded** means the parameter is sent as-is. If the provider
refuses an advisory parameter for a model, LowRouter retries without it
and discloses it as `rejected_by_provider`. After that, the parameter
is dropped up front for that model. If the provider refuses a parameter
that changes the answer, you get its 400.

A provider can also accept a parameter and quietly ignore it. We can't
see that from outside. When the exact shape of the answer matters,
check the answer itself.

### Providers that refuse unknown fields

Most OpenAI-compatible providers ignore a field they do not use.
Mistral and OVHcloud do not: their request schema is closed, and a
field outside it fails the whole request. LowRouter knows which fields
each of them refuses. On those routes the fields are **removed** before
the request leaves, and each one is disclosed as
`unsupported_on_this_path`:

| Provider | Removed and disclosed |
|---|---|
| Mistral | `user`, `name` on a message (disclosed as `messages.name`), a passed-back reasoning trace (`messages.reasoning_content`) |
| Groq | a passed-back reasoning trace (`messages.reasoning_content`) |
| OVHcloud | `user`, `metadata`, `top_k`, `repetition_penalty`, `service_tier`, `prediction` |

The answer is unaffected: `user` only labels the request for the
provider's own abuse tracking, and a message `name` only labels a
participant. A field the provider spells differently is applied rather
than removed. Mistral's `random_seed` receives your `seed`.

Some models refuse a field their provider otherwise accepts. For
example, Mistral models served by Berget and OVHcloud refuse a message
`name`. In that case, or when a provider starts refusing another
advisory field, LowRouter reads the field from the provider's
validation error. It retries without the field and discloses it as
`rejected_by_provider`. When the error is a structured report, as
Mistral's is, the field is also removed up front on later requests.

A refused field that changes the answer, such as `logit_bias` on
Mistral, is never removed. You get the 400, naming the field.

On `/v1/messages`, the Anthropic `metadata` object reaches the provider
as `user`. The disclosure names it `metadata`, which is what you sent.
`/v1/responses` does the same with its own names: see
[the Responses API](responses-api).

### `cache_control`

A `cache_control` object on a content part is Anthropic's explicit
prompt-cache breakpoint. Anthropic routes (including Claude on Bedrock
and Vertex) apply it, TTL included, and Bedrock turns it into its own
cache point. The other routes have no per-block breakpoint: an
OpenAI-compatible provider caches automatically or not at all (and
Mistral rejects the key outright), and Gemini's caching is a separate
resource. On those routes the marker is **removed** before the
request leaves and disclosed as `cache_control` /
`unsupported_on_this_path`. The answer is unaffected; the disclosure
tells you that no cache discount applies on this route for that
reason. The usage block's `prompt_tokens_details.cached_tokens` says
what actually was cached.

### `tool_result` with an image or document

A tool result can carry an image or a document: on the OpenAI surface
as `image_url` / `file` parts in a `tool`-role message, on `/v1/messages`
as `image` / `document` blocks inside a `tool_result`. This is how Claude
Code delivers every screenshot and PDF its `Read` tool opens. Anthropic
routes (including Claude on Bedrock and Vertex) and Bedrock take those
blocks inside the tool result natively and get them as sent. Every
OpenAI-compatible provider and Gemini have a text-only tool result, so
on those routes the text stays in the result and the image or document
is **moved** into a user message placed immediately after it. The model
still sees the file, as a user turn rather than as the tool's own
output, and the move is disclosed as `tool_result` /
`unsupported_on_this_path`, with the number of blocks moved.

### `service_tier`

Only `auto` and `default` are forwarded. Any other value, such as
`priority`, `flex` or `scale`, is **removed** before the request leaves
and disclosed as `service_tier` / `unsupported_on_this_path`. Those
tiers have their own rates at the provider, and LowRouter bills every
request at the model's standard rate. The request is served at the
standard tier, and that is what you pay for.

### `temperature`, `top_p` and `top_k` on Anthropic

Anthropic's newer Claude models refuse the sampling parameters: a
request that sets `temperature`, `top_p` or `top_k` gets a 400 from the
provider, whatever the value. Many clients set `temperature` by
default, so LowRouter does not pass that 400 on. It retries without the
parameter and discloses it as `rejected_by_provider`. After that, the
parameter is dropped up front for that model. Models that accept the
parameters still receive them.

LowRouter learns this from the provider's response, not from the model
name, so there is nothing to configure. Two refusals still come back to
you as the provider's 400, because neither means the model refuses the
parameter: a value outside the provider's range (Anthropic's
`temperature` stops at 1), and a combination the model does not allow
(`temperature` together with `top_p` on some models).

### `stream_options`

`stream_options.include_usage` is accepted for compatibility and has no
effect: the final frame of every stream carries one complete `usage`
object (token counts, `cost`, `currency` and `remaining_balance`) plus
`lowrouter_metadata`, and it is sent whether the flag is `true`, `false`
or absent. It is the only place a streamed request can report what it
cost, so it is not optional. The frame has an empty `choices` array; a
client that indexes `choices[0]` unconditionally must skip it (the SDK
pages show the guard).

### `response_format`

A `json_schema` schema is passed to the provider without changes.
Anthropic, Gemini and Bedrock apply it natively as structured output.
`json_object` needs a JSON mode that works without a schema. Anthropic
and Bedrock have none, so on those routes send `json_schema` with a
schema instead.

### `reasoning_effort`

Accepted values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`.
Each provider gets its own form of the setting:

- **Anthropic** runs extended thinking. Claude 4.6 and later get
  adaptive thinking at the matching effort level. Earlier models get a
  token budget: 1024 / 2048 / 8192 / 16384 / 32768 tokens from
  `minimal` up to `xhigh`. LowRouter works out which form a model takes
  from its response and remembers it, so there is nothing to configure.
  If you did not set `max_tokens`, LowRouter raises its default to leave
  room for the thinking.
- **Gemini** uses thinking levels on Gemini 3 and a thinking budget on
  Gemini 2.5, chosen the same way.
- **OpenAI-compatible** routes receive `reasoning_effort` unchanged.
- **OpenAI's newer reasoning models** (the gpt-5.6 and gpt-6 families)
  don't take function tools together with reasoning on chat
  completions. LowRouter sends those tool requests to OpenAI's Responses
  API instead, asking OpenAI not to store them, so you keep both the
  tools and the reasoning. A request that can't be expressed there (it
  sets `n`, `logprobs`, `logit_bias`, `stop`, a penalty, `seed`, `top_k`
  or `prediction`, sends audio, or returns an image or document from a
  tool) is served with reasoning off, and `reasoning_effort` is
  disclosed as `rejected_by_provider`. These models take no
  `temperature` or `top_p` with reasoning on; those are disclosed as
  `rejected_by_provider` and the reasoning is kept.

`none` asks for nothing beyond the model's default and is never
disclosed. The reasoning tokens are billed as output tokens.

### The reasoning trace

On OpenAI-compatible routes, the model's reasoning trace comes back in
`reasoning_content`, and `reasoning` carries the same text. They appear
on the message of a response and on the deltas of a stream. Providers
send the trace in several ways: under either of those names, as a list
of reasoning details, as thinking parts inside `content`, or as a
`<think>` block at the start of the answer. LowRouter returns it in the
same form whichever route served the request. `content` is always a
string, or absent from a delta. A message without a trace carries
neither field, so a client never reads an empty trace in place of a
real one.

To keep a model's reasoning across tool calls, send the trace back on
the assistant message, under either name. LowRouter passes it to the
model as `reasoning_content`. Where the trace cannot be sent, the
request goes without it and the trace is disclosed as
`messages.reasoning_content`:

- Mistral and Groq refuse the field, so it is removed up front
  (`unsupported_on_this_path`, see the table above).
- Anthropic, Gemini, Bedrock and requests served through OpenAI's
  Responses API have no field for it (`unsupported_on_this_path`).
- Any other route that refuses it with a validation report naming the
  field is retried without it (`rejected_by_provider`).

What this does not cover: Anthropic and Gemini routes do not return
the trace on chat completions, and neither does a provider that keeps
its trace hidden. In both cases the reasoning tokens are still billed
as output tokens, and are reported in
`usage.completion_tokens_details.reasoning_tokens` when the provider
counts them separately.

## Usage details

`usage` breaks the token counts down in OpenAI's own field names, so
clients that show cache hit rates and reasoning cost read them without
any changes:

```json
"usage": {
  "prompt_tokens": 8409,
  "completion_tokens": 212,
  "total_tokens": 8621,
  "prompt_tokens_details": { "cached_tokens": 8192, "cache_write_tokens": 0 },
  "completion_tokens_details": { "reasoning_tokens": 0 },
  "cost": 0.000793,
  "currency": "EUR"
}
```

These are the counts the request was **billed on**, so the cost always
matches them. `reasoning_tokens` is filled in when the provider reports
reasoning tokens separately; otherwise they are included in
`completion_tokens`. The 5-minute and 1-hour split of cache writes is
on `lowrouter_metadata`.

## Balance endpoints

OpenRouter-compatible clients display your balance by calling these two
endpoints:

- `GET /v1/key` returns `usage` (all-time spend) and `limit_remaining`
  (your current balance). `limit` is `null` because LowRouter keys spend
  your account balance rather than a per-key cap.
- `GET /v1/credits` returns `total_credits` and `total_usage`. The
  difference between them is your current balance.

Amounts are in your billing currency, named by `currency`. Unlike
`GET /v1/auth/key`, an empty balance returns 200 with a zero figure
instead of a 402.
