reasoning

package
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 15, 2026 License: AGPL-3.0 Imports: 4 Imported by: 0

Documentation

Overview

Package reasoning owns every decision about extended thinking: what a user may select for a model, and what a call actually sends upstream. Both answers come from this package and share the same internals, so "what the picker offers" and "what the wire receives" cannot drift apart — the drift that produced a web picker showing Off on models that could not be turned off, and hiding it on models that could.

It is a leaf package on purpose. Everything it needs arrives as plain strings and slices, so any caller can use it: turn orchestration, the subagent spawn path, slash commands, HTTP handlers. Nothing here imports another Memoh package.

Index

Constants

View Source
const (
	ModeAdaptive     = "adaptive"
	ModeToggle       = "toggle"
	ModeAlways       = "always"
	ModeOnlyAdaptive = "only_adaptive"
	ModeNone         = "none"
)

ThinkingMode describes how a model's extended-thinking control behaves. It is the capability-discovery output that the UI and wire layer key off of.

  • toggle: user can turn thinking on/off (most reasoning/hybrid models, incl. OpenAI). "off" wire behavior is provider-specific (see adaptor).
  • adaptive: user can turn thinking on/off; when on, the provider uses adaptive thinking (Claude 4.6+/4.7/4.8).
  • always: the model reasons and exposes no control at all — no tiers, no off switch (deepseek-reasoner, MiniMax M2.x). Distinct from toggle: a toggle model with an empty tier list can still be turned off, while this one cannot be influenced in any way.
  • only_adaptive: legacy alias for adaptive retained for branch-local imports.
  • none: model has no thinking concept.

An empty value means "unknown" and is treated as a transitional state that falls back to the legacy reasoning compatibility flag (see ResolveMode).

View Source
const (
	EffortMinimal = "minimal"
	EffortLow     = "low"
	EffortMedium  = "medium"
	EffortHigh    = "high"
	EffortXHigh   = "xhigh"
	EffortMax     = "max"
)

Active effort tiers, weakest to strongest. These are the values that turn reasoning on; "off" is EffortDisable and deliberately not among them.

View Source
const (
	// DialectTier sends a named tier: OpenAI reasoning.effort, Anthropic
	// output_config.effort, Gemini 3.x thinkingLevel.
	DialectTier = "tier"
	// DialectBudget sends a token budget: Anthropic <=4.5 budget_tokens, Gemini
	// 2.5 thinkingBudget.
	DialectBudget = "budget"
)

Reasoning wire dialects. A dialect is how a provider spells the thinking control on the request, which is not derivable from the tiers a model advertises — Gemini 2.5 takes a token budget while 3.x takes a named level, and sending both is a 400. It is declared per model rather than sniffed from an id.

View Source
const (
	// OffSupportUnset means the catalog has not said. Callers fall back to the
	// mode-based rule, which is right for the toggle generation and conservative
	// for the rest.
	OffSupportUnset = ""
	// OffSupportAccepted means the model accepts an explicit disable at any effort.
	OffSupportAccepted = "accepted"
	// OffSupportLowEffortOnly means the model accepts an explicit disable, but only
	// at effort high or below — pairing it with xhigh or max is a 400 (Opus 5).
	OffSupportLowEffortOnly = "low_effort_only"
	// OffSupportRejected means the model always thinks and rejects an explicit
	// disable (Fable 5, Mythos 5). Offering an off switch would be a dead control
	// that also costs a round trip.
	OffSupportRejected = "rejected"
)

How a model responds to an explicit request to stop thinking. This cannot be derived from the thinking mode or the advertised tiers: Anthropic's own per-model table splits models that share both. Opus 4.6 through 4.8 default to thinking off and accept thinking{type:"disabled"}; Sonnet 5 defaults to on and still accepts it; Opus 5 accepts it only at effort high or below; Fable 5 and Mythos 5 reject it outright. Guessing from a model id is what this field exists to avoid.

View Source
const ClientTypeAnthropicMessages = "anthropic-messages"

ClientTypeAnthropicMessages is the one client type this package must recognise by name, because the Anthropic wire changed shape between model generations and the era has to be inferred from the advertised tiers. Callers pass their own client-type string; this constant is what it is compared against.

View Source
const EffortAdaptive = "adaptive"

EffortAdaptive is a legacy per-message override value. Adaptive is a thinking mode resolved from the model, not a tier a caller selects, but settings written before that distinction existed still carry it, so ResolveConfig accepts it as "on, at the default tier".

View Source
const EffortDisable = "disable"

EffortDisable is the single representation of "no reasoning". It is both what a user picks and what a model advertises: a model listing it in reasoning_efforts can be turned off, whatever wire shape that takes on its provider. Since bots dropped the separate reasoning_enabled flag, it is also the only stored form of "off".

View Source
const EffortNone = "none"

EffortNone is OpenAI's wire spelling of "no reasoning" (gpt-5.1 introduced it and dropped minimal; gpt-5.0 has minimal and no none). It is never declared by a model nor stored in settings — provider adaptors translate EffortDisable into it, exactly as the Anthropic path translates the same intent into thinking{type:"disabled"}. Giving "off" one name on our side and letting each provider spell it its own way is what keeps a single state from acquiring two selectable tokens.

Variables

This section is empty.

Functions

func BudgetRatio

func BudgetRatio(effort string) (float64, bool)

BudgetRatio returns where a tier sits within a budget range, as a fraction. It reports false for values that are not active tiers, including the off token.

func DefaultOn

func DefaultOn(defaultOn *bool) bool

DefaultOn reports whether omitting the thinking field leaves the model thinking.

This is a different question from whether a model can be turned off, and the two were briefly conflated. "Can it be turned off" decides whether a picker offers the control; "does omission mean off" decides what the adaptor must send to honour that choice. Claude 4.6 answers no to the second (omitting is off) while Opus 5 answers yes (omitting keeps thinking, billed and counted against max_tokens, while the user believes it is off).

nil means unknown, which callers treat as the conservative "omission is not a reliable way to turn thinking off".

func IsDeclarable

func IsDeclarable(effort string) bool

IsDeclarable reports whether effort can be stored in a model's advertised effort list.

func IsDisabled

func IsDisabled(effort string) bool

IsDisabled reports whether an effort value means "no reasoning". EffortNone is accepted as the legacy spelling: it was declarable and storable before "off" was unified onto EffortDisable.

func IsValidDialect

func IsValidDialect(dialect string) bool

IsValidDialect reports whether a dialect token can be stored. An empty dialect is valid and means "the provider's modern default".

func IsValidMode

func IsValidMode(mode string) bool

IsValidMode reports whether mode can be stored in a model config.

func IsValidOffSupport

func IsValidOffSupport(support string) bool

IsValidOffSupport reports whether an off-support token can be stored.

func NearestToMedium

func NearestToMedium(levels []string) string

NearestToMedium picks the tier closest to medium from levels, breaking ties toward the weaker tier. It is the fallback when a model does not advertise medium: [minimal low] -> low, [high max] -> high, [low high] -> low. Ignores values outside the known tier list (including "disable"), and returns "" when levels has no usable tier.

func NormalizeAdvertised

func NormalizeAdvertised(efforts []string) []string

NormalizeAdvertised rewrites the legacy spelling of "off" to the token a model declares today, and drops duplicates. It runs on both boundaries of a stored model config — before a write is validated and after a row is read back — so nothing downstream has to know that "none" was ever declarable.

Without it the vocabulary change would only apply to freshly written configs: rows persisted earlier, and provider registries that have not been regenerated, would keep advertising "none", and every consumer that now looks for the disable token would read those models as "cannot be turned off" — silently dropping Off from the picker and misreading which thinking mechanism the model wants.

func NormalizeSelection

func NormalizeSelection(selection string, opts Options) (string, bool)

NormalizeSelection validates a user-facing reasoning choice against the resolved options for one model and returns the canonical stored value. "off" and the legacy wire spelling "none" both store as EffortDisable.

func OrderedEfforts

func OrderedEfforts() []string

OrderedEfforts returns the active tiers weakest to strongest. Callers that need a rendering order with "off" at the front prepend EffortDisable themselves — it is not a tier and stays out of this list.

func ReconcileStored

func ReconcileStored(stored string, opts Options) string

ReconcileStored returns the effort a caller should hold after the model changed. A stored value the model still offers survives; "off" survives when off is still reachable; anything else lands on the model's default tier. It returns "" when the model has no thinking concept, meaning the caller should clear the value.

This is the one policy the frontend used to implement twice — once in the bot settings page and once in the chat composer, with the two disagreeing about whether Claude could be turned off.

func ResolveMode

func ResolveMode(declared string, hasReasoningCompat bool, modelID string) string

ResolveMode returns the effective thinking mode, bridging legacy data. The declared mode wins when it is known; only_adaptive collapses onto adaptive.

When the mode is empty — a model imported before the thinking-mode schema existed, or synced from a gateway that carries no capability metadata — the bridge infers one. A model id that reads as Claude 4.6+ (or as a Claude whose version cannot be parsed at all) resolves to adaptive: those generations reject the legacy thinking wire with a 400, and unknown Claude ids skew new because new models keep shipping while old ones retire. Every other id falls back to the old rule — the legacy "reasoning" compatibility flag means toggle, its absence means none — since non-Claude models on this bridge are typically compatibility gateways built around the older, more widely implemented wire shape.

func Supported

func Supported(mode string) bool

Supported reports whether a resolved mode allows any thinking at all.

Types

type Config

type Config struct {
	Active    bool
	Disabled  bool
	Adaptive  bool
	Effort    string
	OffEffort string
}

Config is the resolved reasoning decision for one call.

Active and Disabled are the two on/off states; both false means the model has no thinking concept and the caller sends nothing. Effort is the tier to send when Active. Adaptive selects the Anthropic 4.6+ wire. OffEffort is the value an OpenAI-format provider needs to express "off", or "" when the model cannot be turned off and the field must be omitted entirely.

func ResolveConfig

func ResolveConfig(mode string, advertised []string, options Options, stored, requested, clientType string) *Config

ResolveConfig makes the single reasoning decision for a call.

It takes the same Options projection that capability surfaces render. This is important for CanDisable: explicit model declarations can override both an advertised disable token and a client-type omission fallback, so deriving that answer again here would let the picker and provider wire disagree.

mode:       the model's resolved thinking mode (see ResolveMode)
advertised: the model's declared effort list, "" entries and all
options:    the model's resolved selectable options
stored:     the bot's persisted effort ("" when unset, "disable" for off)
requested:  this message's override ("" when absent)
clientType: the provider's client type, for wire policy

Returns nil when the model has no thinking concept at all.

type Options

type Options struct {
	// Supported is false when the model has no thinking concept; the other fields
	// are then empty and no control should be rendered at all.
	Supported bool `json:"supported"`
	// CanDisable reports whether picking "off" actually reaches the model.
	CanDisable bool `json:"can_disable"`
	// Efforts are the selectable active tiers, weakest to strongest as advertised.
	Efforts []string `json:"efforts,omitempty"`
	// DefaultEffort is the tier to use when nothing is stored, and the tier to fall
	// back to when a stored value is no longer offered by the model.
	DefaultEffort string `json:"default_effort,omitempty"`
	// EffortsWithoutOff are tiers that cannot be combined with off on this model.
	// Opus 5 accepts an explicit disable only at effort high or below, so a client
	// that lets a user hold both must know which tiers conflict.
	EffortsWithoutOff []string `json:"efforts_without_off,omitempty"`
}

Options is what a caller may select for a model: the answer every surface needs before it can render a picker, a slash-command choice list, or an API response.

Efforts holds active tiers only — the disable token never appears in it. Whether off is reachable is CanDisable, a field of its own, so no consumer has to remember to filter a sentinel out of a tier list. That sentinel was how a capability boolean travelled through the effort list before this package existed, and forgetting to filter it is what let an *active* config resolve to "off" in two separate places.

func OptionsFor

func OptionsFor(mode string, advertised []string, clientType, offSupport string) Options

OptionsFor reports what a caller may select for a model.

It shares effectiveEfforts and pickEffort with ResolveConfig on purpose: the options a user is offered and the value a call actually sends are then two readings of one computation, and cannot disagree. Every past bug in this area was a disagreement between those two answers.

offSupport is the model's declared response to an explicit disable (see the OffSupport constants). Empty falls back to the mode-based rule.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL