Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions components/model/claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ func main() {
Enable: true,
BudgetTokens: 1024,
}))
// Adaptive thinking (Claude Opus 4.7 and newer):
// claude.WithThinking(&claude.Thinking{
// Enable: true,
// Mode: claude.ThinkingModeAdaptive,
// Effort: "medium", // "low" | "medium" | "high" | "max" | "xhigh"
// })
if err != nil {
log.Printf("Generate error: %v", err)
return
Expand Down
6 changes: 6 additions & 0 deletions components/model/claude/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ func main() {
Enable: true,
BudgetTokens: 1024,
}))
// 自适应思考(Claude Opus 4.7 及更新模型):
// claude.WithThinking(&claude.Thinking{
// Enable: true,
// Mode: claude.ThinkingModeAdaptive,
// Effort: "medium", // "low" | "medium" | "high" | "max" | "xhigh"
// })
if err != nil {
log.Printf("Generate error: %v", err)
return
Expand Down
76 changes: 69 additions & 7 deletions components/model/claude/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,39 @@ const (
ToolSearchAlgorithmRegex ToolSearchAlgorithm = "regex"
)

// ThinkingMode selects which thinking variant is sent to the API.
//
// Claude Opus 4.7 and newer models reject the legacy {"type":"enabled"}
// shape and instead require {"type":"adaptive"} with an optional
// output_config.effort. Older models (Opus 4.5 / Sonnet 4.5 / 3.7) still
// use the enabled+budget_tokens shape. Keeping both paths lets callers
// opt into the right one per deployment without forking.
type ThinkingMode string

const (
// ThinkingModeEnabled emits the legacy extended-thinking request:
// {"type":"enabled","budget_tokens":N}. Default for backward compat.
ThinkingModeEnabled ThinkingMode = "enabled"
// ThinkingModeAdaptive emits {"type":"adaptive"}; combine with Effort
// to control reasoning depth.
ThinkingModeAdaptive ThinkingMode = "adaptive"
)

type Thinking struct {
Enable bool `json:"enable"`
BudgetTokens int `json:"budget_tokens"`
Enable bool `json:"enable"`

// Mode selects the thinking variant. Defaults to ThinkingModeEnabled
// when empty, preserving the pre-existing behavior.
Mode ThinkingMode `json:"mode,omitempty"`

// BudgetTokens only applies when Mode is empty or ThinkingModeEnabled.
// Ignored for adaptive thinking — the server picks the budget.
BudgetTokens int `json:"budget_tokens,omitempty"`

// Effort sets output_config.effort on the request. Accepted values:
// "low", "medium", "high", "max", "xhigh". Optional; mostly relevant
// for adaptive thinking on Opus 4.7+.
Effort string `json:"effort,omitempty"`
}

type ChatModel struct {
Expand Down Expand Up @@ -608,11 +638,23 @@ func (cm *ChatModel) genMessageNewParams(input []*schema.Message, opts ...model.
}

if specOptions.Thinking != nil && specOptions.Thinking.Enable {
params.Thinking = anthropic.ThinkingConfigParamUnion{
OfEnabled: &anthropic.ThinkingConfigEnabledParam{
Type: "enabled",
BudgetTokens: int64(specOptions.Thinking.BudgetTokens),
},
switch specOptions.Thinking.Mode {
case "", ThinkingModeEnabled:
params.Thinking = anthropic.ThinkingConfigParamUnion{
OfEnabled: &anthropic.ThinkingConfigEnabledParam{
Type: "enabled",
BudgetTokens: int64(specOptions.Thinking.BudgetTokens),
},
}
case ThinkingModeAdaptive:
params.Thinking = anthropic.ThinkingConfigParamUnion{
OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{Type: "adaptive"},
}
default:
return anthropic.MessageNewParams{}, fmt.Errorf("unsupported thinking mode: %s", specOptions.Thinking.Mode)
}
if effort := normalizeOutputEffort(specOptions.Thinking.Effort); effort != "" {
params.OutputConfig = anthropic.OutputConfigParam{Effort: effort}
}
}

Expand Down Expand Up @@ -1501,3 +1543,23 @@ func getEnvWithFallbacks(keys ...string) string {
}
return ""
}

// normalizeOutputEffort maps a user-provided effort string to the
// anthropic SDK's typed enum. Returns an empty value if the string
// is empty or unrecognized, so callers can skip setting the field.
func normalizeOutputEffort(v string) anthropic.OutputConfigEffort {
switch strings.ToLower(strings.TrimSpace(v)) {
case "low":
return anthropic.OutputConfigEffortLow
case "medium":
return anthropic.OutputConfigEffortMedium
case "high":
return anthropic.OutputConfigEffortHigh
case "max":
return anthropic.OutputConfigEffortMax
case "xhigh":
return anthropic.OutputConfigEffortXhigh
default:
return ""
}
}
101 changes: 101 additions & 0 deletions components/model/claude/claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,107 @@ func TestWithTools(t *testing.T) {
assert.Equal(t, "test tool name", ncm.(*ChatModel).origTools[0].Name)
}

func TestThinkingConfig(t *testing.T) {
clearAnthropicAuthEnv(t)

ctx := context.Background()
cm, err := NewChatModel(ctx, &Config{
APIKey: "test-key",
Model: "claude-opus-4-7",
})
assert.NoError(t, err)

t.Run("legacy enabled mode emits OfEnabled with budget", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, BudgetTokens: 1024}),
)
assert.NoError(t, err)
assert.NotNil(t, params.Thinking.OfEnabled)
assert.Equal(t, int64(1024), params.Thinking.OfEnabled.BudgetTokens)
assert.Nil(t, params.Thinking.OfAdaptive)
assert.Empty(t, string(params.OutputConfig.Effort))
}) // preserves backward compatibility for callers already in prod

t.Run("empty mode defaults to OfEnabled", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, BudgetTokens: 512, Mode: ""}),
)
assert.NoError(t, err)
assert.NotNil(t, params.Thinking.OfEnabled)
assert.Nil(t, params.Thinking.OfAdaptive)
})

t.Run("adaptive mode emits OfAdaptive without budget", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, Mode: ThinkingModeAdaptive}),
)
assert.NoError(t, err)
assert.NotNil(t, params.Thinking.OfAdaptive)
assert.Equal(t, "adaptive", string(params.Thinking.OfAdaptive.Type))
assert.Nil(t, params.Thinking.OfEnabled)
})

t.Run("adaptive mode with Effort sets OutputConfig", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, Mode: ThinkingModeAdaptive, Effort: "medium"}),
)
assert.NoError(t, err)
assert.NotNil(t, params.Thinking.OfAdaptive)
assert.Equal(t, anthropic.OutputConfigEffortMedium, params.OutputConfig.Effort)
})

t.Run("Effort is case-insensitive and trimmed", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, Mode: ThinkingModeAdaptive, Effort: " HIGH "}),
)
assert.NoError(t, err)
assert.Equal(t, anthropic.OutputConfigEffortHigh, params.OutputConfig.Effort)
})

t.Run("xhigh Effort sets OutputConfig", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, Mode: ThinkingModeAdaptive, Effort: "xhigh"}),
)
assert.NoError(t, err)
assert.Equal(t, anthropic.OutputConfigEffortXhigh, params.OutputConfig.Effort)
})

t.Run("unknown Effort is ignored (no OutputConfig set)", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, Mode: ThinkingModeAdaptive, Effort: "turbo"}),
)
assert.NoError(t, err)
assert.Empty(t, string(params.OutputConfig.Effort))
})

t.Run("unknown Mode returns error", func(t *testing.T) {
_, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: true, Mode: ThinkingMode("typo")}),
)
assert.Error(t, err)
assert.ErrorContains(t, err, "unsupported thinking mode: typo")
})

t.Run("Enable=false skips thinking entirely", func(t *testing.T) {
params, err := cm.genMessageNewParams(
[]*schema.Message{schema.UserMessage("hi")},
WithThinking(&Thinking{Enable: false, Mode: ThinkingModeAdaptive, Effort: "medium"}),
)
assert.NoError(t, err)
assert.Nil(t, params.Thinking.OfEnabled)
assert.Nil(t, params.Thinking.OfAdaptive)
assert.Empty(t, string(params.OutputConfig.Effort))
})
}

func TestPopulateContentBlockBreakPoint(t *testing.T) {
block := anthropic.NewTextBlock("input")
populateContentBlockBreakPoint(block, nil)
Expand Down
8 changes: 5 additions & 3 deletions components/model/claude/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/cloudwego/eino-ext/components/model/claude
go 1.23.0

require (
github.com/anthropics/anthropic-sdk-go v1.26.0
github.com/anthropics/anthropic-sdk-go v1.46.0
github.com/aws/aws-sdk-go-v2/config v1.29.1
github.com/aws/aws-sdk-go-v2/credentials v1.17.54
github.com/bytedance/mockey v1.2.13
Expand All @@ -30,7 +30,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
Expand All @@ -47,6 +47,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
Expand All @@ -61,6 +62,7 @@ require (
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/smartystreets/goconvey v1.8.1 // indirect
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
Expand All @@ -79,7 +81,7 @@ require (
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.189.0 // indirect
Expand Down
16 changes: 10 additions & 6 deletions components/model/claude/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJ
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
github.com/anthropics/anthropic-sdk-go v1.46.0 h1:yl3n+el5ZfNgiCtQ7zQ7s/NXxB11YbrKXdc3uLPNWlU=
github.com/anthropics/anthropic-sdk-go v1.46.0/go.mod h1:bx5vWuHFuGPkELH8Z4KUiNSohFnUwScdpTyr+50myPo=
github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs=
github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
Expand Down Expand Up @@ -41,8 +41,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
Expand Down Expand Up @@ -123,6 +123,8 @@ github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
Expand Down Expand Up @@ -173,6 +175,8 @@ github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGB
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 h1:uOfcYT+3QungH6tIGSVCR/Y3KJmgJiHcojJbMTPDZAI=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
Expand Down Expand Up @@ -256,8 +260,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
Loading