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