github.com/anthropics/anthropic-sdk-go@v1.70.1/betaparse.go (about) 1 package anthropic 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "reflect" 8 "sync" 9 10 "github.com/invopop/jsonschema" 11 ) 12 13 // ErrStructuredOutputParse is returned (wrapped) by [BetaMessageService.New] 14 // when the API request succeeds but the response body can't be unmarshaled 15 // into the struct pointer passed as Schema. The *BetaMessage is still 16 // returned in that case — check it alongside the error: 17 // 18 // msg, err := client.Beta.Messages.New(ctx, params) 19 // if errors.Is(err, anthropic.ErrStructuredOutputParse) { 20 // // msg is valid; the model's response didn't match the struct shape. 21 // } 22 var ErrStructuredOutputParse = errors.New("anthropic: failed to parse structured output") 23 24 // schemaCache caches the final json.RawMessage (post-transform) keyed by 25 // reflect.Type, so repeated requests with the same struct type skip reflection, 26 // transformation, and marshaling. 27 // 28 // Entries are never evicted. This is fine for the common case where a program 29 // uses a fixed set of struct types (typically < 100, ~1KB each). Programs that 30 // synthesize types dynamically via reflect.StructOf with per-request shapes 31 // should precompute their schemas instead of relying on this cache. 32 var schemaCache sync.Map // reflect.Type → json.RawMessage 33 34 // schemaToRaw converts a value to a json.RawMessage JSON schema suitable for the wire. 35 // If v is already a json.RawMessage, it is returned as-is. 36 // If v is a map[string]any, it is marshaled directly to json.RawMessage. 37 // If v is a struct pointer, the SDK reflects the type into a JSON schema, transforms it, 38 // and returns the result as json.RawMessage. 39 func schemaToRaw(v any) (json.RawMessage, error) { 40 if v == nil { 41 return nil, nil 42 } 43 44 switch s := v.(type) { 45 case json.RawMessage: 46 return s, nil 47 case map[string]any: 48 b, err := json.Marshal(s) 49 if err != nil { 50 return nil, fmt.Errorf("anthropic: failed to marshal schema: %w", err) 51 } 52 return json.RawMessage(b), nil 53 } 54 55 val := reflect.ValueOf(v) 56 if val.Kind() != reflect.Ptr || val.IsNil() { 57 return nil, fmt.Errorf("anthropic: Schema must be a non-nil pointer to a struct, map[string]any, or json.RawMessage, got %T", v) 58 } 59 if val.Elem().Kind() != reflect.Struct { 60 return nil, fmt.Errorf("anthropic: Schema must be a pointer to a struct, got pointer to %s", val.Elem().Kind()) 61 } 62 63 // Cache the final json.RawMessage by reflect.Type so repeated requests 64 // with the same struct type skip reflection, transformation, and marshaling. 65 t := val.Elem().Type() 66 if cached, ok := schemaCache.Load(t); ok { 67 return cached.(json.RawMessage), nil 68 } 69 70 reflector := jsonschema.Reflector{DoNotReference: true} 71 schema := reflector.Reflect(val.Interface()) 72 73 // Transform the schema in-place on the typed struct, then marshal once. 74 transformSchema(schema) 75 76 result, err := json.Marshal(schema) 77 if err != nil { 78 return nil, fmt.Errorf("anthropic: failed to marshal JSON schema: %w", err) 79 } 80 81 raw := json.RawMessage(result) 82 schemaCache.Store(t, raw) 83 return raw, nil 84 } 85 86 // outputFormatDest checks both OutputFormat.Schema and OutputConfig.Format.Schema 87 // for a struct pointer and returns it. Returns (nil, false) if neither has one. 88 func outputFormatDest(params BetaMessageNewParams) (any, bool) { 89 for _, schema := range []any{params.OutputFormat.Schema, params.OutputConfig.Format.Schema} { 90 if schema == nil { 91 continue 92 } 93 val := reflect.ValueOf(schema) 94 if val.Kind() == reflect.Ptr && !val.IsNil() && val.Elem().Kind() == reflect.Struct { 95 return schema, true 96 } 97 } 98 return nil, false 99 } 100 101 // parseOutputContent finds the first text content block in the message 102 // and unmarshals it into dest. Errors are wrapped with ErrStructuredOutputParse 103 // so callers can distinguish parse failures via errors.Is. 104 func parseOutputContent(msg *BetaMessage, dest any) error { 105 for _, block := range msg.Content { 106 if block.Type == "text" { 107 if err := json.Unmarshal([]byte(block.Text), dest); err != nil { 108 return fmt.Errorf("%w: %w", ErrStructuredOutputParse, err) 109 } 110 return nil 111 } 112 } 113 return fmt.Errorf("%w: no text content block found in response", ErrStructuredOutputParse) 114 }