Repository navigation
Custom Response Models EN
English | 简体中文
SystemOneAs[T] decodes the payload into a struct you define, so the compiler knows the shape of your answers:
type TicketAnalysis struct {
Model string `json:"model"`
Usage typesafe.Usage `json:"usage"`
Tone typesafe.ChoiceAnswer `json:"tone"` // lifted from answers["tone"]
Billing *typesafe.NoulAnswer `json:"billing"` // optional: a pointer can be absent
Target *typesafe.BoxAnswer `json:"target"` // supports Box object detection
Summary string `json:"summary,omitempty"`
}
ticket, err := typesafe.SystemOneAs[TicketAnalysis](ctx, client, state, typesafe.Questions{
"tone": typesafe.NewChoice(map[string]any{"calm": nil, "angry": nil}),
"billing": typesafe.NewNoul("Is this about billing?"),
"target": typesafe.NewBox("the item in photo"),
}, typesafe.WithCallImages(true), typesafe.WithModel(typesafe.ModelJevPreview))
if err != nil {
return err
}
fmt.Printf("tone=%s billing=%.2f\n", ticket.Tone.Choice, ticket.Billing.Noul)
if ticket.Target != nil {
fmt.Printf("box: Top=%.2f Bottom=%.2f\n", ticket.Target.Box.Top, ticket.Target.Box.Bottom)
}SystemOneAs is a package-level generic function rather than a method because Go does not allow type parameters on methods.
T may declare the envelope fields (model, usage, answers) and, in addition, one field per answer, named after the question. Each such field is populated from the matching entry of answers. A real envelope field is never displaced by an answer of the same name, and answer types this version does not model are dropped, exactly as in Client.SystemOne.
To keep every answer keyed by question name, declare Answers typesafe.Answers \json:"answers"``:
type Analysis struct {
Model string `json:"model"`
Usage typesafe.Usage `json:"usage"`
Answers typesafe.Answers `json:"answers"`
}
analysis, err := typesafe.SystemOneAs[Analysis](ctx, client, state, questions)
for name, answer := range analysis.Answers {
switch typed := answer.(type) {
case *typesafe.NoulAnswer:
fmt.Printf("%s: p(true)=%.2f\n", name, typed.Noul)
case *typesafe.ChoiceAnswer:
fmt.Printf("%s: %s (confidence %.2f)\n", name, typed.Choice, typed.Confidence)
case *typesafe.ScoreAnswer:
fmt.Printf("%s: score=%.2f\n", name, typed.Score)
case *typesafe.BoxAnswer:
fmt.Printf("%s: box=[%.2f, %.2f, %.2f, %.2f]\n",
name, typed.Box.Top, typed.Box.Bottom, typed.Box.Left, typed.Box.Right)
}
}typesafe.Answers decodes each entry from its type field, which a bare map[string]typesafe.Answer cannot do — an interface type carries no information about which concrete answer to build.
Prefer the lifted form: it reads better and names the questions once.
A top-level field is required unless it can represent absence — a pointer, map, slice, interface, function, or channel holds nil for "not reported" — or it is tagged omitempty/omitzero. Untagged, embedded, and unexported fields are not checked.
A missing required field is reported as an *APIResponseValidationError naming the field rather than silently decoding to a zero value. That matters most for probabilities: a missing NoulAnswer.Noul left at 0 would read as "definitely no".
var validation *typesafe.APIResponseValidationError
if errors.As(err, &validation) {
log.Printf("missing or malformed field: %s", validation.FieldPath)
}- 指南
- Guides