Skip to content

Commit 8311c67

Browse files
committed
feat(claude): add structured output support
1 parent f061db7 commit 8311c67

File tree

5 files changed

+134
-6
lines changed

5 files changed

+134
-6
lines changed

components/model/claude/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,70 @@ type Config struct {
187187

188188

189189

190+
## Structured Output
191+
192+
Use `ResponseFormat` to get JSON responses conforming to a schema:
193+
194+
```go
195+
import (
196+
"github.com/cloudwego/eino-ext/components/model/claude"
197+
"github.com/eino-contrib/jsonschema"
198+
)
199+
200+
type ContactInfo struct {
201+
Name string `json:"name" jsonschema:"description=Full name"`
202+
Email string `json:"email" jsonschema:"description=Email address"`
203+
PlanInterest string `json:"plan_interest" jsonschema:"description=Plan type"`
204+
}
205+
206+
r := jsonschema.Reflector{AllowAdditionalProperties: false, DoNotReference: true}
207+
s := r.Reflect(&ContactInfo{})
208+
209+
cm, err := claude.NewChatModel(ctx, &claude.Config{
210+
APIKey: "your-api-key",
211+
Model: "claude-sonnet-4-6-20250514",
212+
MaxTokens: 1024,
213+
ResponseFormat: &claude.ResponseFormat{
214+
Schema: s,
215+
},
216+
})
217+
```
218+
219+
You can also use Eino's `utils.GoStruct2ParamsOneOf` to derive the schema from a Go struct:
220+
221+
```go
222+
import (
223+
"github.com/cloudwego/eino-ext/components/model/claude"
224+
"github.com/cloudwego/eino/components/tool/utils"
225+
)
226+
227+
type ContactInfo struct {
228+
Name string `json:"name" jsonschema:"description=Full name"`
229+
Email string `json:"email" jsonschema:"description=Email address"`
230+
PlanInterest string `json:"plan_interest" jsonschema:"description=Plan type"`
231+
}
232+
233+
params, _ := utils.GoStruct2ParamsOneOf[ContactInfo]()
234+
s, _ := params.ToJSONSchema()
235+
236+
cm, err := claude.NewChatModel(ctx, &claude.Config{
237+
APIKey: "your-api-key",
238+
Model: "claude-sonnet-4-6-20250514",
239+
MaxTokens: 1024,
240+
ResponseFormat: &claude.ResponseFormat{
241+
Schema: s,
242+
},
243+
})
244+
```
245+
246+
The response format can also be set per-request using `WithResponseFormat`:
247+
248+
```go
249+
resp, err := cm.Generate(ctx, messages, claude.WithResponseFormat(&claude.ResponseFormat{
250+
Schema: s,
251+
}))
252+
```
253+
190254
## Examples
191255

192256
See the following examples for more usage:

components/model/claude/claude.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/anthropics/anthropic-sdk-go/vertex"
3535
awsConfig "github.com/aws/aws-sdk-go-v2/config"
3636
"github.com/aws/aws-sdk-go-v2/credentials"
37+
"github.com/eino-contrib/jsonschema"
3738

3839
"github.com/cloudwego/eino/components"
3940

@@ -146,6 +147,7 @@ func NewChatModel(ctx context.Context, config *Config) (*ChatModel, error) {
146147
thinking: config.Thinking,
147148
topK: config.TopK,
148149
topP: config.TopP,
150+
responseFormat: config.ResponseFormat,
149151
disableParallelToolUse: config.DisableParallelToolUse,
150152
}, nil
151153
}
@@ -240,6 +242,10 @@ type Config struct {
240242

241243
DisableParallelToolUse *bool `json:"disable_parallel_tool_use"`
242244

245+
// ResponseFormat specifies the format of the model's response
246+
// Optional. Use for structured outputs
247+
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
248+
243249
// Additional fields to set in the HTTP request header.
244250
AdditionalHeaderFields map[string]string `json:"additional_header_fields"`
245251

@@ -253,6 +259,12 @@ type Thinking struct {
253259
BudgetTokens int `json:"budget_tokens"`
254260
}
255261

262+
// ResponseFormat configures structured JSON output using a JSON schema.
263+
type ResponseFormat struct {
264+
// Schema is the JSON schema that the model's response must conform to.
265+
Schema *jsonschema.Schema `json:"schema"`
266+
}
267+
256268
type ChatModel struct {
257269
cli anthropic.Client
258270

@@ -263,6 +275,7 @@ type ChatModel struct {
263275
topK *int32
264276
topP *float32
265277
thinking *Thinking
278+
responseFormat *ResponseFormat
266279
tools []anthropic.ToolUnionParam
267280
origTools []*schema.ToolInfo
268281
toolChoice *schema.ToolChoice
@@ -527,7 +540,8 @@ func (cm *ChatModel) genMessageNewParams(input []*schema.Message, opts ...model.
527540
specOptions := model.GetImplSpecificOptions(&options{
528541
TopK: cm.topK,
529542
Thinking: cm.thinking,
530-
DisableParallelToolUse: cm.disableParallelToolUse}, opts...)
543+
DisableParallelToolUse: cm.disableParallelToolUse,
544+
ResponseFormat: cm.responseFormat}, opts...)
531545

532546
params := anthropic.MessageNewParams{}
533547
if commonOptions.Model != nil {
@@ -558,6 +572,18 @@ func (cm *ChatModel) genMessageNewParams(input []*schema.Message, opts ...model.
558572
}
559573
}
560574

575+
if specOptions.ResponseFormat != nil && specOptions.ResponseFormat.Schema != nil {
576+
schemaMap, mErr := jsonSchemaToMap(specOptions.ResponseFormat.Schema)
577+
if mErr != nil {
578+
return anthropic.MessageNewParams{}, fmt.Errorf("failed to marshal response format schema: %w", mErr)
579+
}
580+
params.OutputConfig = anthropic.OutputConfigParam{
581+
Format: anthropic.JSONOutputFormatParam{
582+
Schema: schemaMap,
583+
},
584+
}
585+
}
586+
561587
if err = cm.populateTools(&params, commonOptions, specOptions); err != nil {
562588
return anthropic.MessageNewParams{}, err
563589
}

components/model/claude/go.sum

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJ
77
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
88
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
99
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
10+
github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0=
11+
github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
1012
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
1113
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
1214
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
6466
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6567
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6668
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
67-
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
68-
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
6969
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
7070
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
7171
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=
221221
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
222222
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
223223
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
224+
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
225+
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
224226
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
225227
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
226228
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
236238
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
237239
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
238240
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
241+
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
242+
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
239243
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
240244
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
241245
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
246+
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
247+
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
242248
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
243249
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
244250
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
245251
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
246252
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
253+
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
254+
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
247255
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
248256
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
249257
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
253261
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
254262
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
255263
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
264+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
265+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
256266
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
257267
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
268+
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
269+
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
258270
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
259-
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
260271
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
261272
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
273+
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
274+
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
262275
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
263276
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
264277
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
302315
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
303316
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
304317
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
305-
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
306-
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
307318
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
308319
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
309320
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

components/model/claude/option.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type options struct {
2828
DisableParallelToolUse *bool
2929

3030
AutoCacheControl *CacheControl
31+
32+
ResponseFormat *ResponseFormat
3133
}
3234

3335
func WithTopK(k int32) model.Option {
@@ -72,3 +74,10 @@ func WithAutoCacheControl(ctrl *CacheControl) model.Option {
7274
o.AutoCacheControl = ctrl
7375
})
7476
}
77+
78+
// WithResponseFormat sets the response format for structured JSON output.
79+
func WithResponseFormat(rf *ResponseFormat) model.Option {
80+
return model.WrapImplSpecificOptFn(func(o *options) {
81+
o.ResponseFormat = rf
82+
})
83+
}

components/model/claude/utils.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@
1616

1717
package claude
1818

19+
import (
20+
"encoding/json"
21+
22+
"github.com/eino-contrib/jsonschema"
23+
)
24+
25+
func jsonSchemaToMap(s *jsonschema.Schema) (map[string]any, error) {
26+
b, err := json.Marshal(s)
27+
if err != nil {
28+
return nil, err
29+
}
30+
var m map[string]any
31+
if err = json.Unmarshal(b, &m); err != nil {
32+
return nil, err
33+
}
34+
return m, nil
35+
}
36+
1937
func of[T any](v T) *T {
2038
return &v
2139
}

0 commit comments

Comments
 (0)