From 323558ed7229003262b3dfcbd4475afa392b508d Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Wed, 10 Jun 2026 12:40:47 +0200 Subject: [PATCH] feat(llm): ReasoningEffort request option and ErrUnsupported sentinel Groundwork for the provider phase: reasoning levels map to native knobs (OpenAI reasoning_effort, Ollama think); ErrUnsupported marks declared capability mismatches that chains advance past without health penalty. Co-Authored-By: Claude Fable 5 --- chain.go | 2 +- llm/errors.go | 9 ++++++++- llm/request.go | 12 ++++++++++++ majordomo.go | 4 ++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/chain.go b/chain.go index a593e12..42eb2e0 100644 --- a/chain.go +++ b/chain.go @@ -107,7 +107,7 @@ func chainDo[T any](ctx context.Context, c *chain, attempt func(context.Context, class := c.cfg.classify(err) if class == llm.ClassPermanent { - if errors.Is(err, llm.ErrModelNotFound) || c.cfg.AdvanceOnPermanent { + if errors.Is(err, llm.ErrModelNotFound) || errors.Is(err, llm.ErrUnsupported) || c.cfg.AdvanceOnPermanent { // Not a health problem (or policy says keep going): // advance without penalizing the target. failures = append(failures, fmt.Errorf("%s: %w", t.key, err)) diff --git a/llm/errors.go b/llm/errors.go index 985569c..cd78ef5 100644 --- a/llm/errors.go +++ b/llm/errors.go @@ -26,6 +26,13 @@ const ( // condition. Chains advance past it without penalizing the target's health. var ErrModelNotFound = errors.New("model not found") +// ErrUnsupported marks a request the target cannot serve by declaration — +// e.g. images that cannot be normalized to its capabilities, or a feature +// (tools, structured output) it does not support. Permanent for the target, +// but chains advance past it without penalizing health: another element may +// well be able to serve the request. +var ErrUnsupported = errors.New("request unsupported by target") + // APIError is a structured provider error carrying enough context to // classify it and to debug it. type APIError struct { @@ -96,7 +103,7 @@ func Classify(err error) ErrorClass { if errors.Is(err, context.DeadlineExceeded) { return ClassTransient } - if errors.Is(err, ErrModelNotFound) { + if errors.Is(err, ErrModelNotFound) || errors.Is(err, ErrUnsupported) { return ClassPermanent } if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.ECONNRESET) { diff --git a/llm/request.go b/llm/request.go index fbbf6f6..ce07c72 100644 --- a/llm/request.go +++ b/llm/request.go @@ -37,6 +37,12 @@ type Request struct { // StopSequences halt generation when emitted. StopSequences []string + + // ReasoningEffort requests a reasoning/thinking level from models that + // support one: "low", "medium", or "high" (empty = provider default). + // Providers map it to their native knob (OpenAI reasoning_effort, + // Ollama think levels) and ignore it where no mapping exists. + ReasoningEffort string } // Option mutates a Request before it is sent. Options passed to Generate or @@ -88,6 +94,12 @@ func WithStopSequences(stops ...string) Option { return func(r *Request) { r.StopSequences = stops } } +// WithReasoningEffort requests a reasoning/thinking level ("low", "medium", +// "high") from models that support one. +func WithReasoningEffort(level string) Option { + return func(r *Request) { r.ReasoningEffort = level } +} + // Apply returns a copy of the request with all options applied. Providers // and wrappers call this once at the top of Generate/Stream. func (r Request) Apply(opts ...Option) Request { diff --git a/majordomo.go b/majordomo.go index a63a006..9a77942 100644 --- a/majordomo.go +++ b/majordomo.go @@ -76,6 +76,9 @@ const ( // ErrModelNotFound re-exports llm.ErrModelNotFound. var ErrModelNotFound = llm.ErrModelNotFound +// ErrUnsupported re-exports llm.ErrUnsupported. +var ErrUnsupported = llm.ErrUnsupported + // Re-exported content and message constructors. func Text(s string) Part { return llm.Text(s) } func Image(mime string, data []byte) Part { return llm.Image(mime, data) } @@ -96,6 +99,7 @@ func WithTemperature(t float64) Option { return llm.WithTempera func WithTopP(p float64) Option { return llm.WithTopP(p) } func WithMaxTokens(n int) Option { return llm.WithMaxTokens(n) } func WithStopSequences(stops ...string) Option { return llm.WithStopSequences(stops...) } +func WithReasoningEffort(level string) Option { return llm.WithReasoningEffort(level) } // WithModelCapabilities re-exports llm.WithCapabilities for Provider.Model // calls made through this package.