Skip to content

Custom Response Models EN

sungaowei edited this page Oct 16, 2026 · 1 revision

Custom Response Models

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.


Answer Lifting

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.


Keeping the Whole Answers Map

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.


Required Fields

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)
}

Clone this wiki locally