reasoning

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

Documentation

Overview

Package reasoning is the provider-neutral vocabulary for model reasoning control: how hard a model should think, and which wire dialect expresses that to its provider.

It deliberately imports nothing outside the standard library. Both internal/config and internal/provider depend on it, and provider already imports config, so any dependency in the other direction would be a cycle.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CanCarryDialect

func CanCarryDialect(provider string, dialect Dialect) bool

CanCarryDialect reports whether provider's client can actually deliver dialect's wire shape. Every dialect but DialectAnthropicAdaptive always returns true here - see anthropicNativeCapableProviders for why that one is different. internal/config's checkReasoningIsDeliverable calls this after resolving the dialect, so a model entry naming a dialect its provider's client cannot speak is rejected at config-load time instead of reaching a provider client that would marshal something the wire never defined.

func FormatLevels

func FormatLevels(levels []Level) string

FormatLevels renders a declared set for a UI line. It lives here because the session's refusal message and the CLI picker need the same rendering, and two private copies of a join is exactly how they end up disagreeing.

Its input is always a catalog set, which load has already validated against the closed level vocabulary. Anything rendering values that have NOT cleared that gate must use FormatLevelsQuoted.

func FormatLevelsQuoted

func FormatLevelsQuoted(levels []Level) string

FormatLevelsQuoted is the same rendering with every element quoted, for config load errors. Those print straight to stderr and may carry a level exactly as the operator typed it, so an unescaped ANSI sequence in a TOML string would recolour or clear the reader's terminal.

func OutputReserveFloor added in v0.1.1

func OutputReserveFloor(level Level) int

OutputReserveFloor is the conservative per-effort-level output-token stand-in used whenever a computation must reserve room for a completion but has no explicit token count to use. It lives here, not in internal/provider or internal/config, because BOTH must read the exact same number for the same level:

  • internal/provider's wire request layer (effectiveMaxTokens in openai_compat_request.go) uses it as the max_tokens sent on the wire when a request leaves MaxTokens unset, so an always-thinking model (e.g. z.ai's GLM-5.3 family) does not burn a small provider-side default entirely on reasoning tokens before producing any answer.
  • internal/config's prompt-budget layer (EffectiveOutputTokens in prompt_budget.go) must reserve AT LEAST this much context-window room for the completion before packing history, or the wire request above can ask for more completion tokens than the budget left room for, risking a prompt_tokens+max_tokens over-context-window rejection.

internal/provider cannot depend on internal/config for this (provider already imports config, so the reverse would cycle), which is exactly why this package - a leaf both already depend on - is the single source of truth instead of either duplicating the table or one delegating to the other. This heuristic is unverified against live traffic for every provider and should be tuned as real numbers come in.

Types

type Dialect

type Dialect string

Dialect names the wire shape a provider expects for reasoning control. The same Level reaches different providers as different JSON.

const (
	// DialectOpenAI sends a top-level reasoning_effort string.
	DialectOpenAI Dialect = "openai"
	// DialectOpenRouter sends OpenRouter's canonical nested reasoning object.
	DialectOpenRouter Dialect = "openrouter"
	// DialectOpenRouterOnOff sends OpenRouter's canonical reasoning object
	// carrying ONLY enabled true/false - for models with no reasoning_effort
	// surface (e.g. poolside/laguna-s-2.1).
	DialectOpenRouterOnOff Dialect = "openrouter_onoff"
	// DialectThinking sends a thinking object gating the mode on or off.
	DialectThinking Dialect = "thinking"
	// DialectThinkingEffort sends the thinking object plus reasoning_effort,
	// the shape GLM-5.2+ and DeepSeek v4-pro accept for graded depth.
	DialectThinkingEffort Dialect = "thinking_effort"
	// DialectThinkingPreserved sends a thinking object with clear_thinking:false
	// when enabled (z.ai Preserved Thinking). Model-entry opt-in; the factory
	// default stays DialectThinking so standard-PaaS users stay byte-identical.
	// Graded depth is carried the same way as thinking_effort (reasoning_effort
	// alongside the thinking object) so GLM-5.2 multi-level sets remain valid.
	DialectThinkingPreserved Dialect = "thinking_preserved"
	// DialectNone declares that this model has no reasoning surface. It is
	// distinct from unset: it is a deliberate statement, not a missing key.
	DialectNone Dialect = "none"
	// DialectAnthropicAdaptive sends Anthropic's native adaptive-thinking
	// shape: a top-level thinking object (type "adaptive", or "disabled" for
	// Off) plus output_config.effort carrying the graded level. Distinct from
	// DialectThinking (whose thinking object only ever toggles on/off) and
	// from DialectThinkingEffort (whose effort rides a top-level
	// reasoning_effort field): Anthropic's effort is nested under
	// output_config, and its "on" thinking type is "adaptive", not
	// "enabled". See internal/provider/reasoning.go's reasoningBodyFields,
	// which this dialect's wire shape is encoded in alongside every other
	// dialect - there is one encoder, not a second one to keep in step.
	DialectAnthropicAdaptive Dialect = "anthropic_adaptive"
	// DialectReasoningSplit sends a flat top-level reasoning_split:true field
	// (MiniMax-M3). It does not itself enable or disable thinking - that is
	// controlled by MiniMax's separate `thinking` parameter, which this dialect
	// does not touch and is out of scope here; reasoning_split only controls
	// whether already-occurring reasoning is split into its own response field,
	// instead of being interleaved into the main content. Every active Level
	// (including Off, which is still an active Level: Level.Active() is
	// l != "", and Off's string value is non-empty) produces byte-identical
	// JSON - there is no depth signal to carry and no distinct off-shape.
	// CanGrade's existing default case correctly returns false for it without
	// any new code, since grading it would let /effort report a change the
	// request never made.
	DialectReasoningSplit Dialect = "reasoning_split"
)

func DefaultDialect

func DefaultDialect(provider string) (Dialect, bool)

DefaultDialect returns the vetted wire dialect for a built-in provider. ok=false means the provider has no default and an active level there must name its reasoning_dialect explicitly. Matching is exact, so an unexpected spelling fails closed rather than guessing a wire shape.

func ParseDialect

func ParseDialect(s string) (Dialect, error)

ParseDialect validates a configured dialect. The empty string is accepted and means "use the provider's vetted default, if it has one".

func (Dialect) CanGrade

func (d Dialect) CanGrade() bool

CanGrade reports whether this dialect can put DEPTH on the wire, as opposed to only switching thinking on or off. DialectThinking cannot: its body is a thinking object with one of two types, so every non-Off level it carries produces byte-identical JSON. DialectOpenRouterOnOff cannot either: its wire body carries only enabled true/false. Config uses this to refuse a model that offers graded levels its dialect would flatten, which would leave /effort reporting a change the request never made.

It lives beside the Dialect type rather than beside the request encoder in internal/provider because internal/config must consult it, and config cannot import provider without a cycle. Keep it in step with provider.reasoningBodyFields.

type Level

type Level string

Level is the provider-neutral reasoning dial for one model. The empty Level means unset: no reasoning field is sent at all, which is the required shape for a non-reasoning model. Off is different - it is an explicit instruction to disable thinking, and each dialect has a documented way to say that.

const (
	Off     Level = "off"
	Minimal Level = "minimal"
	Low     Level = "low"
	Medium  Level = "medium"
	High    Level = "high"
	XHigh   Level = "xhigh"
	Max     Level = "max"
	// Auto delegates depth selection to the provider. Some endpoints (e.g.
	// proxied Qwen3.8-flash) reject every graded tier and accept only
	// "auto"; a model entry restricted to [Auto] states exactly that.
	Auto Level = "auto"
)

The closed set of levels. A model that does not accept one of these gets a 400 from its provider naming the values it does accept; embedding a per-model matrix here would rot on every model release.

func ParseLevel

func ParseLevel(s string) (Level, error)

ParseLevel validates a configured level. The empty string is accepted and means unset. Matching is exact: every other closed TOML object in this repo is spelling-strict, and one forgiving key would be a surprising exception.

func (Level) Active

func (l Level) Active() bool

Active reports whether this level instructs the provider at all. Only the empty level is inactive; Off is an active instruction to disable thinking.

type Setting

type Setting struct {
	Level   Level
	Dialect Dialect
}

Setting is one model's resolved reasoning configuration, carried together so the many request paths thread one value instead of two parallel fields that can drift apart.

func Resolve

func Resolve(provider string, s Setting) Setting

Resolve returns the setting with the dialect the wire will actually carry: the configured one when the model named it, otherwise the provider's vetted default. An empty dialect on the way out means the provider has no default and this setting sends nothing, which callers must treat as such rather than guessing a wire shape.

This is the only implementation of that sequencing, and it lives here for the reason Dialect.CanGrade does: internal/config validates it, internal/provider encodes it, and internal/chat reports it, but config cannot import provider without a cycle. Every copy of the rule is a chance for the request path and the surface describing it to disagree about what was sent.

func (Setting) Active

func (s Setting) Active() bool

Active reports whether this setting instructs the provider. A Dialect alone declares a capability for a model that is currently dialled off and sends nothing on its own.

Jump to

Keyboard shortcuts

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