diff --git a/components/model/claude/README.md b/components/model/claude/README.md index 518777ee8..0c62d2c84 100644 --- a/components/model/claude/README.md +++ b/components/model/claude/README.md @@ -187,6 +187,70 @@ type Config struct { +## Structured Output + +Use `ResponseFormat` to get JSON responses conforming to a schema: + +```go +import ( + "github.com/cloudwego/eino-ext/components/model/claude" + "github.com/eino-contrib/jsonschema" +) + +type ContactInfo struct { + Name string `json:"name" jsonschema:"description=Full name"` + Email string `json:"email" jsonschema:"description=Email address"` + PlanInterest string `json:"plan_interest" jsonschema:"description=Plan type"` +} + +r := jsonschema.Reflector{AllowAdditionalProperties: false, DoNotReference: true} +s := r.Reflect(&ContactInfo{}) + +cm, err := claude.NewChatModel(ctx, &claude.Config{ + APIKey: "your-api-key", + Model: "claude-sonnet-4-6-20250514", + MaxTokens: 1024, + ResponseFormat: &claude.ResponseFormat{ + Schema: s, + }, +}) +``` + +You can also use Eino's `utils.GoStruct2ParamsOneOf` to derive the schema from a Go struct: + +```go +import ( + "github.com/cloudwego/eino-ext/components/model/claude" + "github.com/cloudwego/eino/components/tool/utils" +) + +type ContactInfo struct { + Name string `json:"name" jsonschema:"description=Full name"` + Email string `json:"email" jsonschema:"description=Email address"` + PlanInterest string `json:"plan_interest" jsonschema:"description=Plan type"` +} + +params, _ := utils.GoStruct2ParamsOneOf[ContactInfo]() +s, _ := params.ToJSONSchema() + +cm, err := claude.NewChatModel(ctx, &claude.Config{ + APIKey: "your-api-key", + Model: "claude-sonnet-4-6-20250514", + MaxTokens: 1024, + ResponseFormat: &claude.ResponseFormat{ + Schema: s, + }, +}) +``` + +The response format can also be set per-request using `WithResponseFormat`: + +```go +resp, err := cm.Generate(ctx, messages, claude.WithResponseFormat(&claude.ResponseFormat{ + Schema: s, +})) +``` + ## Examples See the following examples for more usage: diff --git a/components/model/claude/claude.go b/components/model/claude/claude.go index a2bf0fe5a..915173e98 100644 --- a/components/model/claude/claude.go +++ b/components/model/claude/claude.go @@ -34,6 +34,7 @@ import ( "github.com/anthropics/anthropic-sdk-go/vertex" awsConfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/eino-contrib/jsonschema" "github.com/cloudwego/eino/components" @@ -146,6 +147,7 @@ func NewChatModel(ctx context.Context, config *Config) (*ChatModel, error) { thinking: config.Thinking, topK: config.TopK, topP: config.TopP, + responseFormat: config.ResponseFormat, disableParallelToolUse: config.DisableParallelToolUse, }, nil } @@ -240,6 +242,10 @@ type Config struct { DisableParallelToolUse *bool `json:"disable_parallel_tool_use"` + // ResponseFormat specifies the format of the model's response + // Optional. Use for structured outputs + ResponseFormat *ResponseFormat `json:"response_format,omitempty"` + // Additional fields to set in the HTTP request header. AdditionalHeaderFields map[string]string `json:"additional_header_fields"` @@ -253,6 +259,12 @@ type Thinking struct { BudgetTokens int `json:"budget_tokens"` } +// ResponseFormat configures structured JSON output using a JSON schema. +type ResponseFormat struct { + // Schema is the JSON schema that the model's response must conform to. + Schema *jsonschema.Schema `json:"schema"` +} + type ChatModel struct { cli anthropic.Client @@ -263,6 +275,7 @@ type ChatModel struct { topK *int32 topP *float32 thinking *Thinking + responseFormat *ResponseFormat tools []anthropic.ToolUnionParam origTools []*schema.ToolInfo toolChoice *schema.ToolChoice @@ -527,7 +540,8 @@ func (cm *ChatModel) genMessageNewParams(input []*schema.Message, opts ...model. specOptions := model.GetImplSpecificOptions(&options{ TopK: cm.topK, Thinking: cm.thinking, - DisableParallelToolUse: cm.disableParallelToolUse}, opts...) + DisableParallelToolUse: cm.disableParallelToolUse, + ResponseFormat: cm.responseFormat}, opts...) params := anthropic.MessageNewParams{} if commonOptions.Model != nil { @@ -558,6 +572,18 @@ func (cm *ChatModel) genMessageNewParams(input []*schema.Message, opts ...model. } } + if specOptions.ResponseFormat != nil && specOptions.ResponseFormat.Schema != nil { + schemaMap, mErr := jsonSchemaToMap(specOptions.ResponseFormat.Schema) + if mErr != nil { + return anthropic.MessageNewParams{}, fmt.Errorf("failed to marshal response format schema: %w", mErr) + } + params.OutputConfig = anthropic.OutputConfigParam{ + Format: anthropic.JSONOutputFormatParam{ + Schema: schemaMap, + }, + } + } + if err = cm.populateTools(¶ms, commonOptions, specOptions); err != nil { return anthropic.MessageNewParams{}, err } diff --git a/components/model/claude/go.sum b/components/model/claude/go.sum index dcc9ffc17..265914e7a 100644 --- a/components/model/claude/go.sum +++ b/components/model/claude/go.sum @@ -7,6 +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.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0= +github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= 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/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs= @@ -64,8 +66,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= @@ -221,6 +221,8 @@ golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -236,14 +238,20 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -253,12 +261,17 @@ 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 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= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -302,8 +315,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/components/model/claude/option.go b/components/model/claude/option.go index 6a95ca2cb..08efee90e 100644 --- a/components/model/claude/option.go +++ b/components/model/claude/option.go @@ -28,6 +28,8 @@ type options struct { DisableParallelToolUse *bool AutoCacheControl *CacheControl + + ResponseFormat *ResponseFormat } func WithTopK(k int32) model.Option { @@ -72,3 +74,10 @@ func WithAutoCacheControl(ctrl *CacheControl) model.Option { o.AutoCacheControl = ctrl }) } + +// WithResponseFormat sets the response format for structured JSON output. +func WithResponseFormat(rf *ResponseFormat) model.Option { + return model.WrapImplSpecificOptFn(func(o *options) { + o.ResponseFormat = rf + }) +} \ No newline at end of file diff --git a/components/model/claude/utils.go b/components/model/claude/utils.go index 8d42ea74f..3cdb56951 100644 --- a/components/model/claude/utils.go +++ b/components/model/claude/utils.go @@ -16,6 +16,24 @@ package claude +import ( + "encoding/json" + + "github.com/eino-contrib/jsonschema" +) + +func jsonSchemaToMap(s *jsonschema.Schema) (map[string]any, error) { + b, err := json.Marshal(s) + if err != nil { + return nil, err + } + var m map[string]any + if err = json.Unmarshal(b, &m); err != nil { + return nil, err + } + return m, nil +} + func of[T any](v T) *T { return &v }