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
- func BudgetRatio(effort string) (float64, bool)
- func DefaultOn(defaultOn *bool) bool
- func IsDeclarable(effort string) bool
- func IsDisabled(effort string) bool
- func IsValidDialect(dialect string) bool
- func IsValidMode(mode string) bool
- func IsValidOffSupport(support string) bool
- func NearestToMedium(levels []string) string
- func NormalizeAdvertised(efforts []string) []string
- func NormalizeSelection(selection string, opts Options) (string, bool)
- func OrderedEfforts() []string
- func ReconcileStored(stored string, opts Options) string
- func ResolveMode(declared string, hasReasoningCompat bool, modelID string) string
- func Supported(mode string) bool
- type Config
- type Options
Constants ¶
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).
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.
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.
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.
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.
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".
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".
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 ¶
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 ¶
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 ¶
IsDeclarable reports whether effort can be stored in a model's advertised effort list.
func IsDisabled ¶
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 ¶
IsValidDialect reports whether a dialect token can be stored. An empty dialect is valid and means "the provider's modern default".
func IsValidMode ¶
IsValidMode reports whether mode can be stored in a model config.
func IsValidOffSupport ¶
IsValidOffSupport reports whether an off-support token can be stored.
func NearestToMedium ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
Types ¶
type Config ¶
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 ¶
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.