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  }