Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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: high by default · Proxy/clients: fields under config

The two knobs

KnobValuesMeaning
reasoning_effortnone | low | medium | high | xhigh | maxRequested thinking or reasoning depth
reasoning_modestandard (default) | proRequests 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:

  1. 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.
  2. Use the provider's native dialect. Pass an exact native object through x-openai, x-anthropic, or x-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

unifiedGPT-6 AstraGPT-6 Sol / LunaGPT-5.6 Sol / Terra / Luna (explicit legacy)
nonelownonenone
lowlowlowlow
mediummediummediummedium
highhighhighhigh
xhighxhighxhighxhigh
maxmaxmaxmax

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}:

unifiedFable 5.1 / Opus 5.5 / Sonnet 5.5Opus 5 / Sonnet 5
noneadaptive + lowthinking: {type: "disabled"}
lowadaptive + lowadaptive + low
mediumadaptive + mediumadaptive + medium
highadaptive + highadaptive + high
xhighadaptive + xhighadaptive + xhigh
maxadaptive + maxadaptive + 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:

unifiedbudget
noneno thinking key
low25% of max_tokens
medium50%
high75%
xhigh90%
maxmax_tokens - 1

Gemini

Gemini uses the four-rung generationConfig.thinkingConfig.thinkingLevel enum:

unifiedGemini 3.5 Flash-LiteGemini 3.8 Flash
noneminimal (zero thinking tokens)low (this model cannot disable thinking)
lowlowlow
mediummediummedium
highhighhigh
xhighhighhigh
maxhighhigh

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}:

unifiedGrok 4.7
nonelow
lowlow
mediummedium
highhigh
xhighxhigh
maxxhigh

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 / modelWhat pro does
OpenAI GPT-5.6 and GPT-6 familiesNative reasoning.mode: "pro"
AnthropicOne-tier effort bump (low → medium → high → xhigh → max)
GeminiOne-tier bump within its four-rung enum, capped at high
xAI Grok 4.7One-tier bump, capped at xhigh

Rules that hold across providers:

  • standard is the default and is not sent on the wire.
  • An explicit effort of none wins over pro; a request to turn reasoning off is not bumped back on, except where the model itself cannot disable it.
  • pro without an effort lets OpenAI native-mode models select their own effort. Other models behave as a default medium bumped to high.

The native GPT-6 mode follows the OpenAI reasoning guide.

Precedence

  1. Native passthrough under x-openai, x-anthropic, or x-gemini wins and is not translated into another dialect. Anthropic also accepts top-level thinking and output_config in the Rust contract.
  2. Otherwise, unified reasoning_effort and reasoning_mode are mapped and clamped according to the tables above.
  3. 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.