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 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-06-20 10:19:30 +07:00
co-authored by Claude Opus 4.8
parent ca9283b93e
commit 7b01517bcd
4 changed files with 115 additions and 9 deletions
@@ -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() {
@@ -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"])
}
+22
View File
@@ -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
+25 -5
View File
@@ -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