reasoning

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package reasoning provides primitives for working with reasoning content.

This package implements utilities for extracting and processing reasoning (step-by-step thinking) content from language model responses. It operates in two modes:

  1. Streaming mode: Uses a stateful ChunkContentSplitter to process content chunks as they arrive, separating text from reasoning blocks marked with <think> or <thinking> tags. The splitter maintains state between calls to handle cases where reasoning blocks span multiple chunks.

  2. Complete content mode: Uses SplitContent function to separate reasoning content from a complete response, handling cases where the LLM backend hasn't explicitly divided reasoning from regular content.

The package recognizes reasoning content enclosed in <think>...</think> or <thinking>...</thinking> tags, which various LLM providers use to indicate step-by-step thinking processes.

Beyond content splitting, the package is the low-level source of truth for model capabilities, shared by every provider adapter so the wire shape is resolved the same way everywhere. It sits below llms and must not import it. It provides:

  • Reasoning-model detection: IsReasoningModel / DefaultIsReasoningModel.
  • Per-family capability resolvers keyed by the distinctive part of the model name (so they work with and without an OpenRouter provider prefix): ClaudeReasoningKindFor and the Claude_* helpers (adaptive vs budget thinking, sampling rules, effort-with-budget, structured-output support), OpenAIReasoningCapsFor / ClampEffort, and the Gemini* helpers.
  • ResolveOff: the single, provider-aware decision of how to disable thinking (an explicit disable wire, a zero budget, or ErrReasoningOffUnsupported for models that cannot be disabled, e.g. always-on Claude or Bedrock defaults).

Unrecognized models are treated as optimistic pass-through: the provider API, not a local table, is the final arbiter, so a newer model never regresses.

Example usage for streaming mode:

splitter := reasoning.NewChunkContentSplitter()

for chunk := range responseChunks {
    text, reasoning := splitter.Split(chunk)

    if reasoning != "" {
        // Process reasoning content (e.g., display as step-by-step thinking)
        fmt.Println("Reasoning:", reasoning)
    }

    if text != "" {
        // Process regular text content
        fmt.Println("Content:", text)
    }
}

Example usage for complete content mode:

response := "Here's what I found: <thinking>First, I need to analyze the data.
The pattern shows increasing values.</thinking> The trend is clearly upward."

reasoning, content := reasoning.SplitContent(response)

fmt.Println("Reasoning:", reasoning)  // "First, I need to analyze the data. The pattern shows increasing values."
fmt.Println("Content:", content)      // "Here's what I found: The trend is clearly upward."

See also:

  • IsReasoningModel: Checks if a model supports reasoning
  • DefaultIsReasoningModel: Provides the default reasoning model detection logic

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClaudeMutuallyExclusiveSampling

func ClaudeMutuallyExclusiveSampling(model string) bool

ClaudeMutuallyExclusiveSampling reports whether the model returns a 400 when temperature and top_p are set together, so the caller must send at most one.

func ClaudePredatesAdaptive

func ClaudePredatesAdaptive(model string) bool

ClaudePredatesAdaptive reports whether the model is a known pre-adaptive Claude generation, so an adaptive request must be gated (not sent verbatim) rather than optimistically forwarded the way a genuinely newer, unclassified model is.

func ClaudeRejectsSampling

func ClaudeRejectsSampling(model string) bool

ClaudeRejectsSampling reports whether the model rejects temperature/top_p outright (true only for the adaptive-only generation), so sampling params must be dropped even when no thinking is requested.

func ClaudeSupportsEffortWithBudget

func ClaudeSupportsEffortWithBudget(model string) bool

ClaudeSupportsEffortWithBudget reports whether the model accepts output_config.effort together with manual (budget) thinking, so the effort is not lost on the budget path for models that honor it.

func ClaudeSupportsStructuredOutput

func ClaudeSupportsStructuredOutput(model string) bool

ClaudeSupportsStructuredOutput reports whether the model can be asked for schema constrained output. Known-legacy families are rejected; every current model and any unrecognized (newer) name passes through so the provider API stays the final arbiter and the local table never blocks a future model.

func ClaudeSupportsThinking

func ClaudeSupportsThinking(model string) bool

ClaudeSupportsThinking reports whether the model is a known extended-thinking Claude generation (any tier except Unknown).

func ClaudeThinkingAlwaysOn

func ClaudeThinkingAlwaysOn(model string) bool

ClaudeThinkingAlwaysOn reports whether the model's thinking cannot be disabled (Fable 5 / Mythos 5 — an explicit disable returns 400).

func ClaudeThinkingDefaultsOn

func ClaudeThinkingDefaultsOn(model string) bool

ClaudeThinkingDefaultsOn reports whether the model thinks when thinking is omitted, so disabling it requires an explicit disable rather than omission.

func DefaultIsReasoningModel

func DefaultIsReasoningModel(model string) bool

DefaultIsReasoningModel provides the default reasoning model detection logic. This can be used by LLM implementations that want to extend rather than replace the default detection logic.

func GeminiCanDisable

func GeminiCanDisable(model string) bool

GeminiCanDisable reports whether thinking can be turned off via thinkingBudget:0. Gemini 2.5 Flash/Flash-Lite and Gemma 4 can; Gemini 2.5 Pro and Gemini 3.x (budget:0 is ignored) cannot. Unclassified Google models are treated as disablable (optimistic: attempt it, let the API be the backstop).

func GeminiSupportsThinking

func GeminiSupportsThinking(model string) bool

GeminiSupportsThinking reports whether the model belongs to a Google thinking family: Gemini 2.5, Gemini 3.x, or Gemma 4.

func GeminiUsesThinkingLevel

func GeminiUsesThinkingLevel(model string) bool

GeminiUsesThinkingLevel reports whether the model uses the qualitative thinking_level control (Gemini 3.x), where thinking_budget is deprecated, instead of a token budget. Gemini 3 also recommends running at temperature 1.0.

func IsReasoningModel

func IsReasoningModel(model string) bool

IsReasoningModel returns true if the model is a reasoning/thinking model. This includes OpenAI o1/o3/GPT-5 series, Anthropic Claude 3.7+, DeepSeek reasoner, etc. For runtime checking of LLM instances, use SupportsReasoningModel instead.

func ResolveClaudeAdaptive

func ResolveClaudeAdaptive(model string, adaptivePreferred bool) bool

ResolveClaudeAdaptive returns whether to send adaptive thinking (true) or budget thinking (false) for a Claude model, given the caller's preference (adaptivePreferred is true when the caller used WithAdaptiveReasoning).

The rule is deterministic and honors the caller's preference whenever the model supports it, falling back only where the preferred mechanism would be rejected:

  • AdaptiveOnly → always adaptive (budget would 400).
  • BudgetOnly → always budget (adaptive would 400).
  • AdaptiveAndBudget / Unknown → the caller's preference, unchanged.

So a currently-accepted call keeps its mechanism; only a currently-rejected (400) combination is redirected to the mechanism the model accepts.

func SplitContent

func SplitContent(content string) (string, string)

Types

type ChunkContentSplitter

type ChunkContentSplitter interface {
	Split(chunk string) (string, string)
	GetState() ChunkContentSplitterState
}

func NewChunkContentSplitter

func NewChunkContentSplitter() ChunkContentSplitter

type ChunkContentSplitterState

type ChunkContentSplitterState int
const (
	ChunkContentSplitterStateText ChunkContentSplitterState = iota
	ChunkContentSplitterStateReasoning
)

type ClaudeReasoningKind

type ClaudeReasoningKind int

ClaudeReasoningKind classifies how a Claude model accepts extended thinking. It is the single source of truth for adaptive-vs-budget thinking and whether sampling params are permitted, so every provider path resolves the wire shape the same way instead of re-deriving it from scattered model-string checks.

const (
	// ClaudeReasoningUnknown is any model not explicitly classified below — a
	// non-Claude model, a Claude model without extended thinking, or a Claude
	// generation newer than this table. It is handled as literal pass-through
	// (the caller's requested mechanism is sent unchanged), preserving prior
	// behavior so an unclassified model never regresses.
	ClaudeReasoningUnknown ClaudeReasoningKind = iota
	// ClaudeReasoningAdaptiveOnly is the newest generation (Opus 4.7/4.8/5,
	// Sonnet 5, Fable 5, Mythos 5): it accepts thinking.type=adaptive +
	// output_config only, and rejects budget_tokens and temperature/top_p with a 400.
	ClaudeReasoningAdaptiveOnly
	// ClaudeReasoningAdaptiveAndBudget is the transitional generation (Opus 4.6,
	// Sonnet 4.6): it accepts both adaptive and budget thinking and permits
	// sampling params.
	ClaudeReasoningAdaptiveAndBudget
	// ClaudeReasoningBudgetOnly is the extended-thinking generation before
	// adaptive existed (Opus 4.5, Sonnet 4.5, Haiku 4.5): it accepts
	// thinking.type=enabled + budget_tokens and rejects adaptive.
	ClaudeReasoningBudgetOnly
)

func ClaudeReasoningKindFor

func ClaudeReasoningKindFor(model string) ClaudeReasoningKind

ClaudeReasoningKindFor classifies a Claude model string. Matching is case-insensitive and substring-based so provider/region prefixes and -vN:0 suffixes do not affect the result.

type ContentReasoning

type ContentReasoning struct {
	// Content is the reasoning content of the assistant message before the final answer.
	Content string `json:"content,omitempty"`

	// Signature is the signature of the reasoning contents.
	Signature []byte `json:"signature,omitempty"`
}

func SplitContentWithReasoning

func SplitContentWithReasoning(content string) (*ContentReasoning, string)

func (*ContentReasoning) IsEmpty

func (r *ContentReasoning) IsEmpty() bool

func (*ContentReasoning) MarshalJSON

func (r *ContentReasoning) MarshalJSON() ([]byte, error)

func (*ContentReasoning) String

func (r *ContentReasoning) String() string

func (*ContentReasoning) UnmarshalJSON

func (r *ContentReasoning) UnmarshalJSON(data []byte) error

type ErrReasoningOffUnsupported

type ErrReasoningOffUnsupported struct{ Model string }

ErrReasoningOffUnsupported is returned when reasoning is explicitly disabled (WithReasoningDisabled) on a model whose thinking cannot be turned off.

func (*ErrReasoningOffUnsupported) Error

type OffWire

type OffWire int

OffWire is how a provider expresses "thinking off" for a given model. It is the single decision point shared by every adapter, keyed off the same model tables the enable path uses, so Off and On can never classify a model differently.

const (
	// OffOmit sends no reasoning field: the model does not think by default (or the
	// provider needs no explicit signal), so omitting already yields "off".
	OffOmit OffWire = iota
	// OffDisableClaude → Anthropic thinking:{type:"disabled"}.
	OffDisableClaude
	// OffZeroBudget → Google thinkingBudget:0.
	OffZeroBudget
	// OffEffortNone → OpenAI reasoning_effort:"none".
	OffEffortNone
	// OffUnsupported: a known mandatory-thinking model that cannot be disabled
	// (adaptive-only Claude, OpenAI o-series). The adapter returns a typed error.
	OffUnsupported
)

func ResolveOff

func ResolveOff(model string, p Provider) OffWire

ResolveOff decides how to disable thinking for a model on a provider, from the same capability tables the enable path reads. Unknown models get the provider's best-effort disable wire (optimistic: attempt it and let an API error be the backstop) rather than a silent omit, so a caller's explicit "off" is honored wherever the provider can honor it.

type OpenAIReasoningCaps

type OpenAIReasoningCaps struct {
	// Known reports whether the model was explicitly classified.
	Known bool
	// CanDisable reports whether the model accepts reasoning_effort "none".
	CanDisable bool
	// Efforts are the accepted effort levels in ascending order, excluding "none";
	// nil when unknown.
	Efforts []string
}

OpenAIReasoningCaps is a best-effort static projection of which reasoning efforts an OpenAI model accepts, so callers avoid sending a value the API would reject with a 400. It asserts only documented, non-default constraints; every other model returns Known=false and is treated optimistically (send as requested, let the API be the arbiter), preserving prior pass-through behavior.

func OpenAIReasoningCapsFor

func OpenAIReasoningCapsFor(model string) OpenAIReasoningCaps

OpenAIReasoningCapsFor classifies an OpenAI reasoning model. Only models with documented constraints tighter than the general GPT-5 surface are listed; anything else (including newer models) returns Known=false so the caller stays optimistic.

func (OpenAIReasoningCaps) ClampEffort

func (c OpenAIReasoningCaps) ClampEffort(effort string) string

ClampEffort lowers a requested effort to what the model accepts: an effort above the model's ceiling drops to the ceiling, and a model that accepts a single effort (e.g. GPT-5 Pro accepts only "high") pins to it. Unknown models and the empty effort are returned unchanged.

type Provider

type Provider int

Provider identifies the calling provider so ResolveOff can pick the right disable wire for a model. It is passed by the adapter, which always knows it.

const (
	ProviderUnknown Provider = iota
	ProviderAnthropic
	ProviderBedrock
	ProviderOpenAI
	ProviderGoogleAI
)

Jump to

Keyboard shortcuts

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