Bifrost

Reasoning

The canonical reasoning ladder, how it maps to each provider's native controls, and how the catalog declares it.

Set reasoning effort through a common scale, from none to max. Bifrost maps that effort to the selected model's native control: discrete levels, a token budget, or an on/off switch. The catalog defines which controls and levels each model supports.

The canonical ladder

none < minimal < low < medium < high < xhigh < max

A request sets reasoning.effort to one of these seven values. max is a first-class tier above xhigh, not an alias: providers including OpenAI and Anthropic expose models where both values are accepted and have different cost/capability semantics.

Requests are clamped, never rejected

If a model doesn't support the exact requested effort, the gateway snaps it to the nearest level it does support — it never returns an error for an out-of-range effort. The only genuine reasoning error is requesting reasoning.effort on a model whose catalog entry doesn't declare reasoning at all.

The snapping rules:

  • Requesting none: resolves to none only if the model has a literal off switch ("none" is one of its declared levels). If the model has no off switch — it always reasons — the request snaps up to the model's lowest declared level (its "floor"). There's no separate canDisable flag: the presence of "none" in levels is the flag.
  • Requesting a positive effort (minimal..max): resolves to the declared positive level nearest the request, in either direction, the lower one on a tie. A positive request never rounds down into none — asking for some reasoning never turns it off.
  • Omitting reasoning entirely: treated as requesting none — i.e., "don't reason" on a model that can turn it off, or the floor on one that can't. There is no opaque upstream default silently inherited.

This means a new model only ever needs to declare which levels it supports; the gateway's public contract stays exactly the same shape regardless of how sparse or how granular that model's real ladder is. Every clamp is recorded in the operation log (requested vs. effective effort, plus a clamped boolean) — the discrepancy is always observable, so a client is never silently charged for more or less reasoning than the log shows. The response reports the effective effort too; see below.

Declaring it in the catalog

"reasoning": {
  "kind": "openai_effort",
  "levels": ["none", "low", "medium", "high", "xhigh", "max"]
}
  • levels — which rungs of the canonical ladder this model accepts, in any order (sorted internally when snapping). Two conventions worth knowing: a binary on/off toggle is ["none", "high"]; a model that always reasons with no off switch and only one real mode is kind: "fixed" with levels: ["high"].
  • budgets — only for the *_budget kinds: a map from effort level to a token budget ({ "low": 4096, "high": 16000 }). Falls back to a provider-specific default per level if omitted. A budget-based spec that declares max must provide its budget explicitly; the gateway never invents a portable token threshold for a provider-defined maximum tier.
  • upstreamEffortMap — only for synonymous provider labels whose spelling differs from the canonical name. Keys are always canonical; values are the provider's native label. It must never collapse distinct semantic tiers: mapping xhigh to max is invalid. Omitted levels pass through unchanged (canonical name == upstream name).

The reasoning-control kinds

kind tells the adapter which wire mechanism to use; it must be one the target adapter can actually emit (adapter.reasoningKinds), checked at startup — a catalog entry can't declare a kind its own adapter doesn't implement.

kindWire shapeProviders
openai_effortTop-level reasoning_effort: "<level>" (or Responses API reasoning.effort)OpenAI reasoning models
openai_bodyProvider-specific top-level field, typically thinking: {"type": "enabled"|"disabled"}, optionally paired with an effort scalar (effortField, e.g. reasoning_effort)Z.AI, MiniMax, and other OpenAI-compatible APIs with native thinking controls
anthropic_adaptivethinking: {"type": "adaptive", "display": ...} + output_config.effort: "<level>"Anthropic adaptive-reasoning models
anthropic_budgetthinking: {"type": "enabled", "budget_tokens": N, "display": ...} (or {"type": "disabled"} for none)Anthropic budget-based models
gemini_levelthinkingConfig: {"thinkingLevel": "low"|"high", "includeThoughts": true} (xhigh maps to high)Gemini models with discrete levels
gemini_budgetthinkingConfig: {"thinkingBudget": N, "includeThoughts": true} (0 for none)Gemini models with a token budget
chat_template_flagA boolean/string/number flag inside chat_template_kwargs, e.g. {"enable_thinking": true} (Qwen) or {"thinking": true} (Kimi)vLLM-style OpenAI-compatible deployments
fixedNo upstream parameter at all — the model always reasons at its one supported levelAlways-on reasoners with no off switch and no granularity

For openai_body and chat_template_flag, a bodyField/chatTemplateFlag sub-object configures the exact parameter name and on/off values:

"reasoning": {
  "kind": "openai_body",
  "levels": ["none", "high"],
  "bodyField": {
    "param": "thinking",
    "onValue": { "type": "enabled" },
    "offValue": { "type": "disabled" }
  }
}

Visible summaries

When reasoning is active, reasoning.summary controls whether the model's thought process is surfaced back to the client: "auto" (the gateway's implicit default whenever effort is on and summary is unspecified), "none" (explicit opt-out), "concise", or "detailed". Each contract maps this to its native shape — Anthropic's display: "summarized"|"omitted", Gemini's includeThoughts, etc.

For a Responses upstream, Chat Completions emits summaries in delta.reasoning, separating distinct parts with blank lines. delta.provider_specific_fields.openai.responses.reasoning_part carries id (the reasoning item), index (the summary part), and status (streaming or done) so clients can display separate blocks. A part ending does not finish the response. Encrypted continuation state remains in provider_specific_fields.openai.reasoning; clients must return it with the corresponding assistant message. The playground preserves both through the AI SDK.

Per-contract expression

  • /v1/chat/completions and /v1/messages: reasoning_effort (OpenAI) or the provider-native fields above.
  • /v1/responses: reasoning: { effort, summary }.

The effort the served model ran at comes back where each contract has room for it: in /v1/responses the response's own reasoning.effort (the effective value, not the one requested; null when the model does not reason). Chat Completions and Messages have no such field, so it is reasoning_effort inside the opt-in unified_routing block, which every text contract returns.

  • Image generation: reasoning-adjacent but distinct — see quality-to-thinking mapping in Images.

Where this comes from in the sync

The catalog sync tool can draft a reasoning spec from models.dev's own vocabulary, but it can never safely infer the wire mechanics (kind, bodyField, effortField) — those are marked needsHumanReview and gate catalog:validate until a human verifies them against the provider's real API docs.

Next steps

On this page