From 7b01517bcdb8a1ec9cd4e9279d58707f45773263 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 20 Jun 2026 10:19:30 +0700 Subject: [PATCH] feat(bedrock): harden adaptive thinking + add Opus 4.7/4.8 (refs #288) Makes adaptive-thinking models usable safely and adds Opus 4.7/4.8 to the catalog so they are selectable and configurable. - middleware: strip temperature/top_p/top_k and set thinking.display=summarized when rewriting to adaptive. Opus 4.7+ reject sampling params (AWS: "no longer supported", 400) and default display to "omitted" (empty reasoning text). - models.yml: add us.anthropic.claude-opus-4-7 and us.anthropic.claude-opus-4-8, plus a ModelReasoningInfo capability descriptor (mode + allowed efforts) on the adaptive models (4.6/4.7/4.8/sonnet-4.6). - provider backstop: force adaptive for adaptive-only models (Opus 4.7/4.8) regardless of agent config, so selecting them cannot 400 on budget thinking. The capability descriptor is the single source of truth (no model-name regex). Verified: unit tests cover the body rewrite (sampling strip + display); a live Bedrock run of the adaptive path on us.anthropic.claude-opus-4-6-v1 passed 21/23 (the 2 failures were 429 throttling, not the mechanism). Opus 4.7/4.8 could not be exercised live -- this AWS account lacks model access (403), not a code issue; their correctness rests on the AWS docs + the 4.6 live run + unit tests. Co-Authored-By: Claude Opus 4.8 --- .../providers/bedrock/adaptive_thinking.go | 46 +++++++++++++++++-- .../bedrock/adaptive_thinking_test.go | 26 +++++++++++ backend/pkg/providers/bedrock/models.yml | 22 +++++++++ backend/pkg/providers/pconfig/config.go | 30 ++++++++++-- 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/backend/pkg/providers/bedrock/adaptive_thinking.go b/backend/pkg/providers/bedrock/adaptive_thinking.go index a8befd64..390c4878 100644 --- a/backend/pkg/providers/bedrock/adaptive_thinking.go +++ b/backend/pkg/providers/bedrock/adaptive_thinking.go @@ -90,11 +90,21 @@ func rewriteAdaptiveThinkingBody(body []byte, effort string) ([]byte, error) { } thinking["type"] = "adaptive" + // Opus 4.7/4.8 default thinking.display to "omitted" (empty reasoning text); + // force "summarized" so reasoning stays visible in the UI. + thinking["display"] = "summarized" delete(thinking, "budget_tokens") fields["output_config"] = map[string]any{ "effort": effort, } + // Adaptive Claude models reject sampling params (Opus 4.7+ return 400 for any + // temperature/top_p/top_k); drop them from the Converse inferenceConfig. + if inferenceConfig, ok := payload["inferenceConfig"].(map[string]any); ok { + delete(inferenceConfig, "temperature") + delete(inferenceConfig, "topP") + } + updatedBody, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("encode Bedrock request body: %w", err) @@ -108,18 +118,46 @@ func (p *bedrockProvider) prepareCallOptions( opt pconfig.ProviderOptionsType, options []llms.CallOption, ) (context.Context, []llms.CallOption) { - reasoning, ok := p.reasoningConfigForType(opt) - if !ok || reasoning.EffectiveMode() != pconfig.ReasoningModeAdaptive { + if !p.usesAdaptiveThinking(opt) { return ctx, options } - effort := string(reasoning.Effort) - ctx = withAdaptiveThinkingEffort(ctx, effort) + reasoning, _ := p.reasoningConfigForType(opt) + ctx = withAdaptiveThinkingEffort(ctx, string(reasoning.Effort)) options = append(options, llms.WithReasoning(llms.ReasoningHigh, 0)) return ctx, options } +// usesAdaptiveThinking reports whether this agent's call must use adaptive thinking: +// either the agent config selected it, or the agent's model only supports adaptive +// (e.g. Opus 4.7/4.8, where budget thinking returns a 400). +func (p *bedrockProvider) usesAdaptiveThinking(opt pconfig.ProviderOptionsType) bool { + if p.modelReasoningMode(opt) == pconfig.ModelReasoningAdaptiveOnly { + return true + } + + reasoning, ok := p.reasoningConfigForType(opt) + return ok && reasoning.EffectiveMode() == pconfig.ReasoningModeAdaptive +} + +// modelReasoningMode returns the reasoning capability declared in models.yml for +// the model assigned to this agent, or ModelReasoningNone if unknown. +func (p *bedrockProvider) modelReasoningMode(opt pconfig.ProviderOptionsType) pconfig.ModelReasoningMode { + agentConfig := p.agentConfigForType(opt) + if agentConfig == nil || agentConfig.Model == "" { + return pconfig.ModelReasoningNone + } + + for _, m := range p.models { + if m.Name == agentConfig.Model && m.Reasoning != nil { + return m.Reasoning.Mode + } + } + + return pconfig.ModelReasoningNone +} + func (p *bedrockProvider) reasoningConfigForType(opt pconfig.ProviderOptionsType) (pconfig.ReasoningConfig, bool) { agentConfig := p.agentConfigForType(opt) if agentConfig == nil || agentConfig.Reasoning.IsZero() { diff --git a/backend/pkg/providers/bedrock/adaptive_thinking_test.go b/backend/pkg/providers/bedrock/adaptive_thinking_test.go index 2bbbd4e1..e3ac7ab1 100644 --- a/backend/pkg/providers/bedrock/adaptive_thinking_test.go +++ b/backend/pkg/providers/bedrock/adaptive_thinking_test.go @@ -44,3 +44,29 @@ func TestRewriteAdaptiveThinkingBodyWithoutThinking(t *testing.T) { require.NoError(t, err) assert.JSONEq(t, string(body), string(updatedBody)) } + +func TestRewriteAdaptiveThinkingBodyStripsSamplingAndSetsDisplay(t *testing.T) { + body := []byte(`{ + "additionalModelRequestFields": { + "thinking": {"type": "enabled", "budget_tokens": 4096} + }, + "inferenceConfig": { + "maxTokens": 16384, + "temperature": 1.0, + "topP": 0.95 + } + }`) + + updatedBody, err := rewriteAdaptiveThinkingBody(body, "high") + require.NoError(t, err) + + var payload map[string]any + require.NoError(t, json.Unmarshal(updatedBody, &payload)) + + thinking := payload["additionalModelRequestFields"].(map[string]any)["thinking"].(map[string]any) + assert.Equal(t, "adaptive", thinking["type"]) + assert.Equal(t, "summarized", thinking["display"]) + + // Opus 4.7+ reject sampling params; only maxTokens must survive in inferenceConfig. + assert.Equal(t, map[string]any{"maxTokens": float64(16384)}, payload["inferenceConfig"]) +} diff --git a/backend/pkg/providers/bedrock/models.yml b/backend/pkg/providers/bedrock/models.yml index b656f95e..2cabf128 100644 --- a/backend/pkg/providers/bedrock/models.yml +++ b/backend/pkg/providers/bedrock/models.yml @@ -43,6 +43,9 @@ - name: us.anthropic.claude-opus-4-7 description: Most capable Opus model for advanced software engineering, long-running agentic tasks, professional work, and rigorous security analysis with adaptive thinking thinking: true + reasoning: + mode: adaptive-only + efforts: [low, medium, high, xhigh, max] release_date: 2026-04-16 price: input: 5.0 @@ -50,10 +53,26 @@ cache_read: 0.5 cache_write: 6.25 +- name: us.anthropic.claude-opus-4-8 + description: Anthropic Opus 4.8 - flagship model for coding, agents, and deep reasoning in enterprise security workflows, with adaptive thinking only + thinking: true + reasoning: + mode: adaptive-only + efforts: [low, medium, high, xhigh, max] + release_date: 2026-05-28 + price: + input: 5.0 + output: 25.0 + cache_read: 0.5 + cache_write: 6.25 + # Anthropic Claude 4.6 Series - Latest generation with world-class coding and agentic capabilities - name: us.anthropic.claude-opus-4-6-v1 description: World's best model for coding, enterprise agents, and professional work with industry-leading reliability for agentic workflows and security analysis thinking: true + reasoning: + mode: adaptive + efforts: [low, medium, high, max] release_date: 2026-02-05 price: input: 5.0 @@ -64,6 +83,9 @@ - name: us.anthropic.claude-sonnet-4-6 description: Frontier intelligence at scale built for coding, agents, and enterprise workflows with sustained reasoning and adaptive decision-making thinking: true + reasoning: + mode: adaptive + efforts: [low, medium, high, max] release_date: 2026-02-17 price: input: 3.0 diff --git a/backend/pkg/providers/pconfig/config.go b/backend/pkg/providers/pconfig/config.go index 8c9754b7..018e6b25 100644 --- a/backend/pkg/providers/pconfig/config.go +++ b/backend/pkg/providers/pconfig/config.go @@ -169,11 +169,31 @@ var AllAgentTypes = []ProviderOptionsType{ } type ModelConfig struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Description *string `json:"description,omitempty" yaml:"description,omitempty"` - ReleaseDate *time.Time `json:"release_date,omitempty" yaml:"release_date,omitempty"` - Thinking *bool `json:"thinking,omitempty" yaml:"thinking,omitempty"` - Price *PriceInfo `json:"price,omitempty" yaml:"price,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + ReleaseDate *time.Time `json:"release_date,omitempty" yaml:"release_date,omitempty"` + Thinking *bool `json:"thinking,omitempty" yaml:"thinking,omitempty"` + Reasoning *ModelReasoningInfo `json:"reasoning,omitempty" yaml:"reasoning,omitempty"` + Price *PriceInfo `json:"price,omitempty" yaml:"price,omitempty"` +} + +// ModelReasoningMode declares a model's reasoning capability. It is distinct from +// the per-agent ReasoningConfig.Mode (the chosen mode): it is the source of truth +// that lets a provider force the correct mode for models that require it. +type ModelReasoningMode string + +const ( + ModelReasoningNone ModelReasoningMode = "" // no extended thinking + ModelReasoningBudget ModelReasoningMode = "budget" // budget thinking only (older Claude) + ModelReasoningAdaptive ModelReasoningMode = "adaptive" // budget or adaptive (Opus 4.6, Sonnet 4.6) + ModelReasoningAdaptiveOnly ModelReasoningMode = "adaptive-only" // adaptive required; budget returns 400 (Opus 4.7/4.8) +) + +// ModelReasoningInfo describes how a model supports extended thinking, so the +// provider and UI can pick a valid mode/effort without hardcoding model names. +type ModelReasoningInfo struct { + Mode ModelReasoningMode `json:"mode,omitempty" yaml:"mode,omitempty"` + Efforts []llms.ReasoningEffort `json:"efforts,omitempty" yaml:"efforts,omitempty"` } type ModelsConfig []ModelConfig