Reasoning controls
llmshim exposes one reasoning vocabulary across providers. You state the desired depth; the selected provider adapter maps it to what that model accepts.
Availability: Rust: top-level fields · CLI:
highby default · Proxy/clients: fields underconfig
The two knobs
| Knob | Values | Meaning |
|---|---|---|
reasoning_effort | none | low | medium | high | xhigh | max | Requested thinking or reasoning depth |
reasoning_mode | standard (default) | pro | Requests substantially more model work, accepting higher latency and cost |
In a Rust request, both are top-level fields:
{
"model": "anthropic/claude-sonnet-5-5",
"messages": [{"role": "user", "content": "Solve this carefully."}],
"reasoning_effort": "high",
"reasoning_mode": "pro"
}
In the proxy contract, put them under config:
{
"model": "anthropic/claude-sonnet-5-5",
"messages": [{"role": "user", "content": "Solve this carefully."}],
"config": {
"reasoning_effort": "high",
"reasoning_mode": "pro"
}
}
Preserve reasoning for replay
Responses carry an ordered message.reasoning array. Every block has kind
(text, redacted, or encrypted) and origin: provider, upstream model,
coarse model family, wire format, receipt time, and an optional issuer binding.
Text blocks carry text; opaque blocks carry data. Signatures, Responses
item ids, original structured payloads and container identity are retained when
present. Preserve the complete assistant message when building the next request.
The proxy exposes the same array inside message; its top-level reasoning
string remains a display-only convenience.
One replay policy applies to all adapters and fallback attempts: the target
family and wire must match, and encrypted blocks also require the same provider
and account binding. Unknown families and missing origins fail closed. Signed
Anthropic blocks precede text and tools in their original order. Responses
requests use store:false, include encrypted reasoning, and replay those items
locally. Native overrides cannot turn storage on or bypass reasoning provenance.
The outbound projection removes both previous_response_id and conversation,
including references supplied through native overrides. Send complete history
in the request: a provider-side conversation can otherwise prepend stored
items, as described in the Responses API reference.
OpenAI and xAI use this stateless Responses path; see
xAI's encrypted reasoning contract.
API-key account bindings hash the endpoint and credential; they conservatively
stop matching after key rotation. OAuth bindings use the account identifier
captured from the actual request. No credential or raw account identifier is
emitted. Rejected replay is silent and increments a bounded-reason counter,
available through llmshim::reasoning::replay_counters().
Gemini tool-call signatures are objects containing data and origin. Keep
that entire object on the tool call. Legacy reasoning_content,
reasoning_signature, redacted_reasoning_content, and string tool signatures
are read for one migration release, but untracked data is dropped. An explicit
reasoning_origin can accompany legacy message fields during migration; new
writers emit only the block array. Models absent from the catalog need a local
family assertion before their reasoning can be replayed.
Inbound reasoning history has a separate whole-request limit of 16 MiB of
estimated derived metadata and 16,384 reasoning records. A record is one
reasoning block or tool signature, rather than every JSON descendant. The limit
is checked across all messages before legacy origins or payloads are repeated;
an excessive request fails with a fixed 400 and no provider call. Accepted
legacy and normalized blocks remain byte-for-byte intact. The public Rust
filter_reasoning_for_target helper returns a Result for the same reason:
propagate a budget error instead of treating it as an ordinary policy drop.
Unary reasoning blocks, tool signatures, wire identities, and their provenance
share a 16 MiB estimated-owned-memory and 4,096-entry derived metadata limit.
The limit is checked before model, provider, account, payload, or signature
copies are created. A response that exceeds it fails as an upstream response;
llmshim never returns a truncated or partially normalized assistant message.
The public Rust reasoning::capture_response helper therefore returns a
Result; callers must discard the response and propagate an error. Successful
capture preserves every accepted origin string byte-for-byte.
Streaming reasoning uses the same blocks with a part index and an optional
replace:true flag for completed item snapshots. ReasoningAccumulator assembles
fragments and removes stream framing before storage:
#![allow(unused)] fn main() { let mut reasoning = llmshim::reasoning::ReasoningAccumulator::default(); // For each normalized chunk: reasoning.push(&chunk["choices"][0]["delta"])?; // When the stream has completed successfully: assistant["reasoning"] = serde_json::json!(reasoning.blocks()); }
push returns a Result because reasoning text, signatures, encrypted data,
and block identities share the per-response retained-state budget. Propagate
that error; an over-budget accumulator never returns an apparently complete
partial result. Each native stream frame also applies the derived metadata
limit before eagerly normalizing multiple completed reasoning blocks; the
retained-state limit remains the separate bound across frames.
Use reasoning_text(&message_or_delta) for display without inspecting opaque
data. A provider may expose only a summary or no readable reasoning at all.
Portable intent or native control
Choose per request:
- Use the unified controls. llmshim maps the requested effort and mode to the selected model family, clamping to the nearest accepted tier when the model has a smaller vocabulary.
- Use the provider's native dialect. Pass an exact native object through
x-openai,x-anthropic, orx-gemini. Native configuration wins over the unified controls.
flowchart LR
U[Unified effort + mode] --> P{Native override present?}
P -- Yes --> N[x-provider native config]
P -- No --> F[Model-family mapping]
F --> C[Clamp to supported tier]
C --> W[Provider-native reasoning control]
See Native provider controls for passthrough examples.
Effort mapping tables
These mappings follow provider documentation and live checks. A bold value is a clamp rather than a direct name-for-name mapping.
OpenAI Responses API
| unified | GPT-6 Astra | GPT-6 Sol / Luna | GPT-5.6 Sol / Terra / Luna (explicit legacy) |
|---|---|---|---|
none | low | none | none |
low | low | low | low |
medium | medium | medium | medium |
high | high | high | high |
xhigh | xhigh | xhigh | xhigh |
max | max | max | max |
OpenAI receives reasoning.effort. Legacy minimal input clamps to low
on the three advertised GPT-6 models. See the
OpenAI model catalog.
Anthropic Messages API
Adaptive models use thinking: {type} plus output_config: {effort}:
| unified | Fable 5.1 / Opus 5.5 / Sonnet 5.5 | Opus 5 / Sonnet 5 |
|---|---|---|
none | adaptive + low | thinking: {type: "disabled"} |
low | adaptive + low | adaptive + low |
medium | adaptive + medium | adaptive + medium |
high | adaptive + high | adaptive + high |
xhigh | adaptive + xhigh | adaptive + xhigh |
max | adaptive + max | adaptive + max |
On models that can disable thinking, reasoning_effort: "none" maps to disabled
thinking. Fable 5.1, Opus 5.5, and Sonnet 5.5 always use adaptive thinking, so
none maps to low. Explicit native disabled/manual thinking is rejected
locally for those models, as is forced tool choice. Opus 5.5 thinking blocks
replay to Fable 5.1, while older Opus models cannot consume them. Sonnet 5.5
blocks replay only to Sonnet 5.5, and Sonnet 5.5 reads thinking from Sonnet 5,
Opus 4.8, Haiku 4.5, and earlier, but not from Opus 5, Opus 5.5, or a
Fable/Mythos model. Opus 5.5 and Sonnet 5.5 omit temperature, top_p, and
top_k, as do Fable and Opus 5. See the
Opus 5.5 migration guide
and the
Sonnet 5.5 model page.
Fable behavior and its five effort levels follow Anthropic's migration guide and Models API. The low-effort and rejected-parameter paths are also checked against the live API in this repository.
Haiku 4.5 uses enabled thinking with a token budget scaled from max_tokens and floored at 1024:
| unified | budget |
|---|---|
none | no thinking key |
low | 25% of max_tokens |
medium | 50% |
high | 75% |
xhigh | 90% |
max | max_tokens - 1 |
Gemini
Gemini uses the four-rung
generationConfig.thinkingConfig.thinkingLevel enum:
| unified | Gemini 3.5 Flash-Lite | Gemini 3.8 Flash |
|---|---|---|
none | minimal (zero thinking tokens) | low (this model cannot disable thinking) |
low | low | low |
medium | medium | medium |
high | high | high |
xhigh | high | high |
max | high | high |
Gemini 3.8 Flash rejects minimal and thinkingBudget: 0, so none clamps
to low. Gemini 3.5 Flash-Lite accepts minimal and can disable thinking.
Native controls remain available through x-gemini.thinkingConfig.
ChatGPT subscription
GPT-6 Astra preserves low, medium, high, xhigh, and max;
none and minimal clamp to low because Astra cannot disable reasoning.
GPT-6 Sol and Luna accept none and max; minimal clamps to low.
The shared Responses translator applies these mappings through both ChatGPT
subscription and API-key OpenAI. See the
official OpenAI model catalog.
Native x-chatgpt.reasoning overrides the unified mapping.
xAI Responses API
xAI receives the nested native shape reasoning: {effort}:
| unified | Grok 4.7 |
|---|---|
none | low |
low | low |
medium | medium |
high | high |
xhigh | xhigh |
max | xhigh |
Grok 4.7 cannot disable reasoning, so none clamps to low.
Grok 4.7 always returns encrypted reasoning on Responses. llmshim preserves
the opaque payload and its provenance in message.reasoning, requests
store: false, and replays compatible payloads unchanged on subsequent turns.
Persist the full assistant message, including tool wire IDs and reasoning;
storing only its text loses this context. Decoding does not modify
origin.model or determine routing.
The adapter translates named tool selection to the Responses shape
{"type":"function","name":"lookup"}. Callers may continue to send the
Chat Completions shape with function.name.
Mode mapping: reasoning_mode: "pro"
| Provider / model | What pro does |
|---|---|
| OpenAI GPT-5.6 and GPT-6 families | Native reasoning.mode: "pro" |
| Anthropic | One-tier effort bump (low → medium → high → xhigh → max) |
| Gemini | One-tier bump within its four-rung enum, capped at high |
| xAI Grok 4.7 | One-tier bump, capped at xhigh |
Rules that hold across providers:
standardis the default and is not sent on the wire.- An explicit effort of
nonewins overpro; a request to turn reasoning off is not bumped back on, except where the model itself cannot disable it. prowithout an effort lets OpenAI native-mode models select their own effort. Other models behave as a defaultmediumbumped tohigh.
The native GPT-6 mode follows the OpenAI reasoning guide.
Precedence
- Native passthrough under
x-openai,x-anthropic, orx-geminiwins and is not translated into another dialect. Anthropic also accepts top-levelthinkingandoutput_configin the Rust contract. - Otherwise, unified
reasoning_effortandreasoning_modeare mapped and clamped according to the tables above. - With neither, the provider/model default applies. The provider decides whether and how much to reason unless an explicit setting is supplied.
The adapter mappings are pinned by provider unit tests. The tables above cover the advertised models; compatibility mappings for explicit older IDs remain in the adapters and tests. Provider capabilities can change, so the implementation and these tables must move together.
Signature observations
The default signature-introspection feature makes a best-effort read of an
undocumented Anthropic signature header. Its public decoder,
providers::anthropic_signature::serving_model_from_signature, returns Option;
unknown formats (including headers without a model), malformed data, and the
feature-disabled stub return None. Tests use synthetic protobufs only.
This never changes origin.model, a stored message, replay eligibility, routing,
fallback, or cache decisions. The decoded value is unverified metadata, not
authentication or proof of which model ran. Dotted/dashed spellings and dated
snapshots compare as the same model line; an unrecognized internal codename may
be reported as a mismatch. Logs record gateway_integrity as ok, mismatch
(with requested/served names), or unknown. Unknowns produce no warning.
A mismatch increments providers::anthropic_signature::mismatch_count() and adds
x-llmshim-served-model outside the assistant message. Streaming adds it to the
terminal canonical chunk or the proxy's done event. Multiple observed names
are comma separated. Disable inspection with --no-default-features; model
requests and stored message contents remain identical, while observation fields
and metrics can differ.
Anthropic effort and budget selection now comes from catalog reasoning_options.
An effort entry selects adaptive thinking and clamps to an advertised tier; a
budget entry selects enabled thinking and respects its minimum, maximum, and
max_tokens - 1. An impossible budget fails before HTTP dispatch. Verified
builtin entries preserve the established mappings; a bounded table of historical
model lines supplies missing metadata. Unknown future names are not guessed.