anthropic.go

   1// Package anthropic provides an implementation of the fantasy AI SDK for Anthropic's language models.
   2package anthropic
   3
   4import (
   5	"cmp"
   6	"context"
   7	"encoding/base64"
   8	"encoding/json"
   9	"errors"
  10	"fmt"
  11	"io"
  12	"maps"
  13	"math"
  14	"strings"
  15
  16	"charm.land/fantasy"
  17	"charm.land/fantasy/object"
  18	"charm.land/fantasy/providers/internal/httpheaders"
  19	"github.com/aws/aws-sdk-go-v2/config"
  20	"github.com/charmbracelet/anthropic-sdk-go"
  21	"github.com/charmbracelet/anthropic-sdk-go/bedrock"
  22	"github.com/charmbracelet/anthropic-sdk-go/option"
  23	"github.com/charmbracelet/anthropic-sdk-go/packages/param"
  24	"github.com/charmbracelet/anthropic-sdk-go/vertex"
  25	"golang.org/x/oauth2/google"
  26)
  27
  28// betaRequestOptions converts beta flag strings into request
  29// options that enable the corresponding Anthropic beta APIs.
  30func betaRequestOptions(flags []string) []option.RequestOption {
  31	if len(flags) == 0 {
  32		return nil
  33	}
  34	opts := []option.RequestOption{option.WithQuery("beta", "true")}
  35	for _, flag := range flags {
  36		opts = append(opts, option.WithHeaderAdd("anthropic-beta", flag))
  37	}
  38	return opts
  39}
  40
  41// buildRequestOptions constructs the common request options shared
  42// by Generate and Stream: user-agent, raw tool injection, and any
  43// beta API flags.
  44func buildRequestOptions(call fantasy.Call, rawTools []json.RawMessage, betaFlags []string) []option.RequestOption {
  45	reqOpts := callUARequestOptions(call)
  46	if len(rawTools) > 0 {
  47		// Tools are injected as raw JSON rather than via params.Tools
  48		// because the SDK doesn't model beta tool types (e.g. computer
  49		// use). If the SDK adds validation that reads params.Tools,
  50		// this will need updating.
  51		reqOpts = append(reqOpts, option.WithJSONSet("tools", rawTools))
  52	}
  53	if len(betaFlags) > 0 {
  54		reqOpts = append(reqOpts, betaRequestOptions(betaFlags)...)
  55	}
  56	return reqOpts
  57}
  58
  59const (
  60	// Name is the name of the Anthropic provider.
  61	Name = "anthropic"
  62	// DefaultURL is the default URL for the Anthropic API.
  63	DefaultURL = "https://api.anthropic.com"
  64	// VertexAuthScope is the auth scope required for vertex auth if using a Service Account JSON file (e.g. GOOGLE_APPLICATION_CREDENTIALS).
  65	VertexAuthScope = "https://www.googleapis.com/auth/cloud-platform"
  66)
  67
  68type options struct {
  69	baseURL   string
  70	apiKey    string
  71	name      string
  72	headers   map[string]string
  73	userAgent string
  74	client    option.HTTPClient
  75
  76	vertexProject  string
  77	vertexLocation string
  78	skipAuth       bool
  79
  80	useBedrock bool
  81
  82	objectMode fantasy.ObjectMode
  83}
  84
  85type provider struct {
  86	options options
  87}
  88
  89// Option defines a function that configures Anthropic provider options.
  90type Option = func(*options)
  91
  92// New creates a new Anthropic provider with the given options.
  93func New(opts ...Option) (fantasy.Provider, error) {
  94	providerOptions := options{
  95		headers:    map[string]string{},
  96		objectMode: fantasy.ObjectModeAuto,
  97	}
  98	for _, o := range opts {
  99		o(&providerOptions)
 100	}
 101
 102	if !providerOptions.useBedrock {
 103		providerOptions.baseURL = cmp.Or(providerOptions.baseURL, DefaultURL)
 104	}
 105	providerOptions.name = cmp.Or(providerOptions.name, Name)
 106	return &provider{options: providerOptions}, nil
 107}
 108
 109// WithBaseURL sets the base URL for the Anthropic provider.
 110func WithBaseURL(baseURL string) Option {
 111	return func(o *options) {
 112		o.baseURL = baseURL
 113	}
 114}
 115
 116// WithAPIKey sets the API key for the Anthropic provider.
 117func WithAPIKey(apiKey string) Option {
 118	return func(o *options) {
 119		o.apiKey = apiKey
 120	}
 121}
 122
 123// WithVertex configures the Anthropic provider to use Vertex AI.
 124func WithVertex(project, location string) Option {
 125	return func(o *options) {
 126		o.vertexProject = project
 127		o.vertexLocation = location
 128	}
 129}
 130
 131// WithSkipAuth configures whether to skip authentication for the Anthropic provider.
 132func WithSkipAuth(skip bool) Option {
 133	return func(o *options) {
 134		o.skipAuth = skip
 135	}
 136}
 137
 138// WithBedrock configures the Anthropic provider to use AWS Bedrock.
 139func WithBedrock() Option {
 140	return func(o *options) {
 141		o.useBedrock = true
 142	}
 143}
 144
 145// WithName sets the name for the Anthropic provider.
 146func WithName(name string) Option {
 147	return func(o *options) {
 148		o.name = name
 149	}
 150}
 151
 152// WithHeaders sets the headers for the Anthropic provider.
 153func WithHeaders(headers map[string]string) Option {
 154	return func(o *options) {
 155		maps.Copy(o.headers, headers)
 156	}
 157}
 158
 159// WithHTTPClient sets the HTTP client for the Anthropic provider.
 160func WithHTTPClient(client option.HTTPClient) Option {
 161	return func(o *options) {
 162		o.client = client
 163	}
 164}
 165
 166// WithUserAgent sets an explicit User-Agent header, overriding the default and any
 167// value set via WithHeaders.
 168func WithUserAgent(ua string) Option {
 169	return func(o *options) {
 170		o.userAgent = ua
 171	}
 172}
 173
 174// WithObjectMode sets the object generation mode.
 175func WithObjectMode(om fantasy.ObjectMode) Option {
 176	return func(o *options) {
 177		// not supported
 178		if om == fantasy.ObjectModeJSON {
 179			om = fantasy.ObjectModeAuto
 180		}
 181		o.objectMode = om
 182	}
 183}
 184
 185func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.LanguageModel, error) {
 186	clientOptions := make([]option.RequestOption, 0, 5+len(a.options.headers))
 187	clientOptions = append(clientOptions, option.WithMaxRetries(0))
 188
 189	if a.options.apiKey != "" && !a.options.useBedrock {
 190		clientOptions = append(clientOptions, option.WithAPIKey(a.options.apiKey))
 191	}
 192	if !a.options.useBedrock && a.options.baseURL != "" {
 193		clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL))
 194	}
 195	defaultUA := httpheaders.DefaultUserAgent(fantasy.Version)
 196	resolved := httpheaders.ResolveHeaders(a.options.headers, a.options.userAgent, defaultUA)
 197	for key, value := range resolved {
 198		clientOptions = append(clientOptions, option.WithHeader(key, value))
 199	}
 200	if a.options.client != nil {
 201		clientOptions = append(clientOptions, option.WithHTTPClient(a.options.client))
 202	}
 203	if a.options.vertexProject != "" && a.options.vertexLocation != "" {
 204		var credentials *google.Credentials
 205		if a.options.skipAuth {
 206			credentials = &google.Credentials{TokenSource: &googleDummyTokenSource{}}
 207		} else {
 208			var err error
 209			credentials, err = google.FindDefaultCredentials(ctx, VertexAuthScope)
 210			if err != nil {
 211				return nil, err
 212			}
 213		}
 214
 215		clientOptions = append(
 216			clientOptions,
 217			vertex.WithCredentials(
 218				ctx,
 219				a.options.vertexLocation,
 220				a.options.vertexProject,
 221				credentials,
 222			),
 223		)
 224	}
 225	if a.options.useBedrock {
 226		modelID = bedrockPrefixModelWithRegion(modelID)
 227
 228		if a.options.skipAuth || a.options.apiKey != "" {
 229			clientOptions = append(
 230				clientOptions,
 231				bedrock.WithConfig(bedrockBasicAuthConfig(a.options.apiKey)),
 232			)
 233		} else {
 234			if cfg, err := config.LoadDefaultConfig(ctx); err == nil {
 235				clientOptions = append(
 236					clientOptions,
 237					bedrock.WithConfig(cfg),
 238				)
 239			}
 240		}
 241		if a.options.baseURL != "" {
 242			clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL))
 243		}
 244	}
 245	return languageModel{
 246		modelID:  modelID,
 247		provider: a.options.name,
 248		options:  a.options,
 249		client:   anthropic.NewClient(clientOptions...),
 250	}, nil
 251}
 252
 253type languageModel struct {
 254	provider string
 255	modelID  string
 256	client   anthropic.Client
 257	options  options
 258}
 259
 260// Model implements fantasy.LanguageModel.
 261func (a languageModel) Model() string {
 262	return a.modelID
 263}
 264
 265// Provider implements fantasy.LanguageModel.
 266func (a languageModel) Provider() string {
 267	return a.provider
 268}
 269
 270func (a languageModel) prepareParams(call fantasy.Call) (
 271	params *anthropic.MessageNewParams,
 272	rawTools []json.RawMessage,
 273	warnings []fantasy.CallWarning,
 274	betaFlags []string,
 275	err error,
 276) {
 277	params = &anthropic.MessageNewParams{}
 278	providerOptions := &ProviderOptions{}
 279	if v, ok := call.ProviderOptions[Name]; ok {
 280		providerOptions, ok = v.(*ProviderOptions)
 281		if !ok {
 282			return nil, nil, nil, nil, &fantasy.Error{Title: "invalid argument", Message: "anthropic provider options should be *anthropic.ProviderOptions"}
 283		}
 284	}
 285	sendReasoning := true
 286	if providerOptions.SendReasoning != nil {
 287		sendReasoning = *providerOptions.SendReasoning
 288	}
 289	systemBlocks, messages, warnings := toPrompt(call.Prompt, sendReasoning)
 290
 291	if call.FrequencyPenalty != nil {
 292		warnings = append(warnings, fantasy.CallWarning{
 293			Type:    fantasy.CallWarningTypeUnsupportedSetting,
 294			Setting: "FrequencyPenalty",
 295		})
 296	}
 297	if call.PresencePenalty != nil {
 298		warnings = append(warnings, fantasy.CallWarning{
 299			Type:    fantasy.CallWarningTypeUnsupportedSetting,
 300			Setting: "PresencePenalty",
 301		})
 302	}
 303
 304	params.System = systemBlocks
 305	params.Messages = messages
 306	params.Model = anthropic.Model(a.modelID)
 307	params.MaxTokens = 4096
 308
 309	if call.MaxOutputTokens != nil {
 310		params.MaxTokens = *call.MaxOutputTokens
 311	}
 312
 313	if call.Temperature != nil {
 314		params.Temperature = param.NewOpt(*call.Temperature)
 315	}
 316	if call.TopK != nil {
 317		params.TopK = param.NewOpt(*call.TopK)
 318	}
 319	if call.TopP != nil {
 320		params.TopP = param.NewOpt(*call.TopP)
 321	}
 322
 323	switch {
 324	case providerOptions.Effort != nil:
 325		effort := *providerOptions.Effort
 326		params.OutputConfig = anthropic.OutputConfigParam{
 327			Effort: anthropic.OutputConfigEffort(effort),
 328		}
 329		adaptive := anthropic.NewThinkingConfigAdaptiveParam()
 330		params.Thinking.OfAdaptive = &adaptive
 331	case providerOptions.Thinking != nil:
 332		if providerOptions.Thinking.BudgetTokens == 0 {
 333			return nil, nil, nil, nil, &fantasy.Error{Title: "no budget", Message: "thinking requires budget"}
 334		}
 335		params.Thinking = anthropic.ThinkingConfigParamOfEnabled(providerOptions.Thinking.BudgetTokens)
 336		if call.Temperature != nil {
 337			params.Temperature = param.Opt[float64]{}
 338			warnings = append(warnings, fantasy.CallWarning{
 339				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 340				Setting: "temperature",
 341				Details: "temperature is not supported when thinking is enabled",
 342			})
 343		}
 344		if call.TopP != nil {
 345			params.TopP = param.Opt[float64]{}
 346			warnings = append(warnings, fantasy.CallWarning{
 347				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 348				Setting: "TopP",
 349				Details: "TopP is not supported when thinking is enabled",
 350			})
 351		}
 352		if call.TopK != nil {
 353			params.TopK = param.Opt[int64]{}
 354			warnings = append(warnings, fantasy.CallWarning{
 355				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 356				Setting: "TopK",
 357				Details: "TopK is not supported when thinking is enabled",
 358			})
 359		}
 360	}
 361
 362	if len(call.Tools) > 0 {
 363		disableParallelToolUse := false
 364		if providerOptions.DisableParallelToolUse != nil {
 365			disableParallelToolUse = *providerOptions.DisableParallelToolUse
 366		}
 367		var toolChoice *anthropic.ToolChoiceUnionParam
 368		var toolWarnings []fantasy.CallWarning
 369		rawTools, toolChoice, toolWarnings, betaFlags = a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse)
 370		if toolChoice != nil {
 371			params.ToolChoice = *toolChoice
 372		}
 373		warnings = append(warnings, toolWarnings...)
 374	}
 375
 376	return params, rawTools, warnings, betaFlags, nil
 377}
 378
 379func (a *provider) Name() string {
 380	return Name
 381}
 382
 383// GetCacheControl extracts cache control settings from provider options.
 384func GetCacheControl(providerOptions fantasy.ProviderOptions) *CacheControl {
 385	if anthropicOptions, ok := providerOptions[Name]; ok {
 386		if options, ok := anthropicOptions.(*ProviderCacheControlOptions); ok {
 387			return &options.CacheControl
 388		}
 389	}
 390	return nil
 391}
 392
 393// GetReasoningMetadata extracts reasoning metadata from provider options.
 394func GetReasoningMetadata(providerOptions fantasy.ProviderOptions) *ReasoningOptionMetadata {
 395	if anthropicOptions, ok := providerOptions[Name]; ok {
 396		if reasoning, ok := anthropicOptions.(*ReasoningOptionMetadata); ok {
 397			return reasoning
 398		}
 399	}
 400	return nil
 401}
 402
 403type messageBlock struct {
 404	Role     fantasy.MessageRole
 405	Messages []fantasy.Message
 406}
 407
 408func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock {
 409	var blocks []*messageBlock
 410
 411	var currentBlock *messageBlock
 412
 413	for _, msg := range prompt {
 414		switch msg.Role {
 415		case fantasy.MessageRoleSystem:
 416			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleSystem {
 417				currentBlock = &messageBlock{
 418					Role:     fantasy.MessageRoleSystem,
 419					Messages: []fantasy.Message{},
 420				}
 421				blocks = append(blocks, currentBlock)
 422			}
 423			currentBlock.Messages = append(currentBlock.Messages, msg)
 424		case fantasy.MessageRoleUser:
 425			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
 426				currentBlock = &messageBlock{
 427					Role:     fantasy.MessageRoleUser,
 428					Messages: []fantasy.Message{},
 429				}
 430				blocks = append(blocks, currentBlock)
 431			}
 432			currentBlock.Messages = append(currentBlock.Messages, msg)
 433		case fantasy.MessageRoleAssistant:
 434			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleAssistant {
 435				currentBlock = &messageBlock{
 436					Role:     fantasy.MessageRoleAssistant,
 437					Messages: []fantasy.Message{},
 438				}
 439				blocks = append(blocks, currentBlock)
 440			}
 441			currentBlock.Messages = append(currentBlock.Messages, msg)
 442		case fantasy.MessageRoleTool:
 443			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
 444				currentBlock = &messageBlock{
 445					Role:     fantasy.MessageRoleUser,
 446					Messages: []fantasy.Message{},
 447				}
 448				blocks = append(blocks, currentBlock)
 449			}
 450			currentBlock.Messages = append(currentBlock.Messages, msg)
 451		}
 452	}
 453	return blocks
 454}
 455
 456func anyToStringSlice(v any) []string {
 457	switch typed := v.(type) {
 458	case []string:
 459		if len(typed) == 0 {
 460			return nil
 461		}
 462		out := make([]string, len(typed))
 463		copy(out, typed)
 464		return out
 465	case []any:
 466		if len(typed) == 0 {
 467			return nil
 468		}
 469		out := make([]string, 0, len(typed))
 470		for _, item := range typed {
 471			s, ok := item.(string)
 472			if !ok || s == "" {
 473				continue
 474			}
 475			out = append(out, s)
 476		}
 477		if len(out) == 0 {
 478			return nil
 479		}
 480		return out
 481	default:
 482		return nil
 483	}
 484}
 485
 486const maxExactIntFloat64 = float64(1<<53 - 1)
 487
 488// asProviderDefinedTool extracts the ProviderDefinedTool from a
 489// Tool, handling both ProviderDefinedTool and
 490// ExecutableProviderTool.
 491func asProviderDefinedTool(tool fantasy.Tool) (fantasy.ProviderDefinedTool, bool) {
 492	if pdt, ok := tool.(fantasy.ProviderDefinedTool); ok {
 493		return pdt, true
 494	}
 495	if ept, ok := tool.(fantasy.ExecutableProviderTool); ok {
 496		return ept.Definition(), true
 497	}
 498	return fantasy.ProviderDefinedTool{}, false
 499}
 500
 501func anyToInt64(v any) (int64, bool) {
 502	switch typed := v.(type) {
 503	case int:
 504		return int64(typed), true
 505	case int8:
 506		return int64(typed), true
 507	case int16:
 508		return int64(typed), true
 509	case int32:
 510		return int64(typed), true
 511	case int64:
 512		return typed, true
 513	case uint:
 514		u64 := uint64(typed)
 515		if u64 > math.MaxInt64 {
 516			return 0, false
 517		}
 518		return int64(u64), true
 519	case uint8:
 520		return int64(typed), true
 521	case uint16:
 522		return int64(typed), true
 523	case uint32:
 524		return int64(typed), true
 525	case uint64:
 526		if typed > math.MaxInt64 {
 527			return 0, false
 528		}
 529		return int64(typed), true
 530	case float32:
 531		f := float64(typed)
 532		if math.Trunc(f) != f || math.IsNaN(f) || math.IsInf(f, 0) || f < -maxExactIntFloat64 || f > maxExactIntFloat64 {
 533			return 0, false
 534		}
 535		return int64(f), true
 536	case float64:
 537		if math.Trunc(typed) != typed || math.IsNaN(typed) || math.IsInf(typed, 0) || typed < -maxExactIntFloat64 || typed > maxExactIntFloat64 {
 538			return 0, false
 539		}
 540		return int64(typed), true
 541	case json.Number:
 542		parsed, err := typed.Int64()
 543		if err != nil {
 544			return 0, false
 545		}
 546		return parsed, true
 547	default:
 548		return 0, false
 549	}
 550}
 551
 552func anyToUserLocation(v any) *UserLocation {
 553	switch typed := v.(type) {
 554	case *UserLocation:
 555		return typed
 556	case UserLocation:
 557		loc := typed
 558		return &loc
 559	case map[string]any:
 560		loc := &UserLocation{}
 561		if city, ok := typed["city"].(string); ok {
 562			loc.City = city
 563		}
 564		if region, ok := typed["region"].(string); ok {
 565			loc.Region = region
 566		}
 567		if country, ok := typed["country"].(string); ok {
 568			loc.Country = country
 569		}
 570		if timezone, ok := typed["timezone"].(string); ok {
 571			loc.Timezone = timezone
 572		}
 573		if loc.City == "" && loc.Region == "" && loc.Country == "" && loc.Timezone == "" {
 574			return nil
 575		}
 576		return loc
 577	default:
 578		return nil
 579	}
 580}
 581
 582func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (rawTools []json.RawMessage, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning, betaFlags []string) {
 583	for _, tool := range tools {
 584		if tool.GetType() == fantasy.ToolTypeFunction {
 585			ft, ok := tool.(fantasy.FunctionTool)
 586			if !ok {
 587				continue
 588			}
 589			required := []string{}
 590			var properties any
 591			if props, ok := ft.InputSchema["properties"]; ok {
 592				properties = props
 593			}
 594			if req, ok := ft.InputSchema["required"]; ok {
 595				if reqArr, ok := req.([]string); ok {
 596					required = reqArr
 597				}
 598			}
 599			cacheControl := GetCacheControl(ft.ProviderOptions)
 600
 601			anthropicTool := anthropic.ToolParam{
 602				Name:        ft.Name,
 603				Description: anthropic.String(ft.Description),
 604				InputSchema: anthropic.ToolInputSchemaParam{
 605					Properties: properties,
 606					Required:   required,
 607				},
 608			}
 609			if cacheControl != nil {
 610				anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
 611			}
 612			raw, err := json.Marshal(anthropic.ToolUnionParam{OfTool: &anthropicTool})
 613			if err != nil {
 614				warnings = append(warnings, fantasy.CallWarning{
 615					Type:    fantasy.CallWarningTypeOther,
 616					Tool:    tool,
 617					Message: fmt.Sprintf("failed to marshal function tool: %v", err),
 618				})
 619				continue
 620			}
 621			rawTools = append(rawTools, raw)
 622			continue
 623		}
 624		if tool.GetType() == fantasy.ToolTypeProviderDefined {
 625			pt, ok := asProviderDefinedTool(tool)
 626			if !ok {
 627				continue
 628			}
 629			switch pt.ID {
 630			case "web_search":
 631				webSearchTool := anthropic.WebSearchTool20250305Param{}
 632				if pt.Args != nil {
 633					if domains := anyToStringSlice(pt.Args["allowed_domains"]); len(domains) > 0 {
 634						webSearchTool.AllowedDomains = domains
 635					}
 636					if domains := anyToStringSlice(pt.Args["blocked_domains"]); len(domains) > 0 {
 637						webSearchTool.BlockedDomains = domains
 638					}
 639					if maxUses, ok := anyToInt64(pt.Args["max_uses"]); ok && maxUses > 0 {
 640						webSearchTool.MaxUses = param.NewOpt(maxUses)
 641					}
 642					if loc := anyToUserLocation(pt.Args["user_location"]); loc != nil {
 643						var ulp anthropic.UserLocationParam
 644						if loc.City != "" {
 645							ulp.City = param.NewOpt(loc.City)
 646						}
 647						if loc.Region != "" {
 648							ulp.Region = param.NewOpt(loc.Region)
 649						}
 650						if loc.Country != "" {
 651							ulp.Country = param.NewOpt(loc.Country)
 652						}
 653						if loc.Timezone != "" {
 654							ulp.Timezone = param.NewOpt(loc.Timezone)
 655						}
 656						webSearchTool.UserLocation = ulp
 657					}
 658				}
 659				raw, err := json.Marshal(anthropic.ToolUnionParam{
 660					OfWebSearchTool20250305: &webSearchTool,
 661				})
 662				if err != nil {
 663					warnings = append(warnings, fantasy.CallWarning{
 664						Type:    fantasy.CallWarningTypeOther,
 665						Tool:    tool,
 666						Message: fmt.Sprintf("failed to marshal web search tool: %v", err),
 667					})
 668					continue
 669				}
 670				rawTools = append(rawTools, raw)
 671				continue
 672			}
 673			if IsComputerUseTool(tool) {
 674				raw, err := computerUseToolJSON(pt)
 675				if err != nil {
 676					warnings = append(warnings, fantasy.CallWarning{
 677						Type:    fantasy.CallWarningTypeOther,
 678						Tool:    tool,
 679						Message: fmt.Sprintf("failed to build computer use tool: %v", err),
 680					})
 681					continue
 682				}
 683				version, ok := getComputerUseVersion(pt)
 684				if ok {
 685					flag, err := computerUseBetaFlag(version)
 686					if err != nil {
 687						warnings = append(warnings, fantasy.CallWarning{
 688							Type:    fantasy.CallWarningTypeOther,
 689							Tool:    tool,
 690							Message: fmt.Sprintf("unsupported computer use version: %v", err),
 691						})
 692						continue
 693					}
 694					betaFlags = append(betaFlags, flag)
 695				}
 696				rawTools = append(rawTools, raw)
 697				continue
 698			}
 699			warnings = append(warnings, fantasy.CallWarning{
 700				Type:    fantasy.CallWarningTypeUnsupportedTool,
 701				Tool:    tool,
 702				Message: "tool is not supported",
 703			})
 704			continue
 705		}
 706		warnings = append(warnings, fantasy.CallWarning{
 707			Type:    fantasy.CallWarningTypeUnsupportedTool,
 708			Tool:    tool,
 709			Message: "tool is not supported",
 710		})
 711	}
 712
 713	// NOTE: Bedrock does not support this attribute.
 714	var disableParallelToolUse param.Opt[bool]
 715	if !a.options.useBedrock {
 716		disableParallelToolUse = param.NewOpt(disableParallelToolCalls)
 717	}
 718
 719	if toolChoice == nil {
 720		if disableParallelToolCalls {
 721			anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 722				OfAuto: &anthropic.ToolChoiceAutoParam{
 723					Type:                   "auto",
 724					DisableParallelToolUse: disableParallelToolUse,
 725				},
 726			}
 727		}
 728		return rawTools, anthropicToolChoice, warnings, betaFlags
 729	}
 730
 731	switch *toolChoice {
 732	case fantasy.ToolChoiceAuto:
 733		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 734			OfAuto: &anthropic.ToolChoiceAutoParam{
 735				Type:                   "auto",
 736				DisableParallelToolUse: disableParallelToolUse,
 737			},
 738		}
 739	case fantasy.ToolChoiceRequired:
 740		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 741			OfAny: &anthropic.ToolChoiceAnyParam{
 742				Type:                   "any",
 743				DisableParallelToolUse: disableParallelToolUse,
 744			},
 745		}
 746	case fantasy.ToolChoiceNone:
 747		none := anthropic.NewToolChoiceNoneParam()
 748		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 749			OfNone: &none,
 750		}
 751	default:
 752		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 753			OfTool: &anthropic.ToolChoiceToolParam{
 754				Type:                   "tool",
 755				Name:                   string(*toolChoice),
 756				DisableParallelToolUse: disableParallelToolUse,
 757			},
 758		}
 759	}
 760	return rawTools, anthropicToolChoice, warnings, betaFlags
 761}
 762
 763func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) {
 764	var systemBlocks []anthropic.TextBlockParam
 765	var messages []anthropic.MessageParam
 766	var warnings []fantasy.CallWarning
 767
 768	blocks := groupIntoBlocks(prompt)
 769	finishedSystemBlock := false
 770	for _, block := range blocks {
 771		switch block.Role {
 772		case fantasy.MessageRoleSystem:
 773			if finishedSystemBlock {
 774				// skip multiple system messages that are separated by user/assistant messages
 775				// TODO: see if we need to send error here?
 776				continue
 777			}
 778			finishedSystemBlock = true
 779			for _, msg := range block.Messages {
 780				for i, part := range msg.Content {
 781					isLastPart := i == len(msg.Content)-1
 782					cacheControl := GetCacheControl(part.Options())
 783					if cacheControl == nil && isLastPart {
 784						cacheControl = GetCacheControl(msg.ProviderOptions)
 785					}
 786					text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 787					if !ok {
 788						continue
 789					}
 790					textBlock := anthropic.TextBlockParam{
 791						Text: text.Text,
 792					}
 793					if cacheControl != nil {
 794						textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 795					}
 796					systemBlocks = append(systemBlocks, textBlock)
 797				}
 798			}
 799
 800		case fantasy.MessageRoleUser:
 801			var anthropicContent []anthropic.ContentBlockParamUnion
 802			for _, msg := range block.Messages {
 803				if msg.Role == fantasy.MessageRoleUser {
 804					for i, part := range msg.Content {
 805						isLastPart := i == len(msg.Content)-1
 806						cacheControl := GetCacheControl(part.Options())
 807						if cacheControl == nil && isLastPart {
 808							cacheControl = GetCacheControl(msg.ProviderOptions)
 809						}
 810						switch part.GetType() {
 811						case fantasy.ContentTypeText:
 812							text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 813							if !ok {
 814								continue
 815							}
 816							textBlock := &anthropic.TextBlockParam{
 817								Text: text.Text,
 818							}
 819							if cacheControl != nil {
 820								textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 821							}
 822							anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 823								OfText: textBlock,
 824							})
 825						case fantasy.ContentTypeSource:
 826							// Source content from web search results is not a
 827							// recognized Anthropic content block type; skip it.
 828							continue
 829						case fantasy.ContentTypeFile:
 830							file, ok := fantasy.AsMessagePart[fantasy.FilePart](part)
 831							if !ok {
 832								continue
 833							}
 834							// TODO: handle other file types
 835							switch {
 836							case strings.HasPrefix(file.MediaType, "image/"):
 837								base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
 838								imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
 839								if cacheControl != nil {
 840									imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
 841								}
 842								anthropicContent = append(anthropicContent, imageBlock)
 843							case strings.HasPrefix(file.MediaType, "text/"):
 844								documentBlock := anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{
 845									Data: string(file.Data),
 846								})
 847								anthropicContent = append(anthropicContent, documentBlock)
 848							}
 849						}
 850					}
 851				} else if msg.Role == fantasy.MessageRoleTool {
 852					for i, part := range msg.Content {
 853						isLastPart := i == len(msg.Content)-1
 854						cacheControl := GetCacheControl(part.Options())
 855						if cacheControl == nil && isLastPart {
 856							cacheControl = GetCacheControl(msg.ProviderOptions)
 857						}
 858						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
 859						if !ok {
 860							continue
 861						}
 862						toolResultBlock := anthropic.ToolResultBlockParam{
 863							ToolUseID: result.ToolCallID,
 864						}
 865						switch result.Output.GetType() {
 866						case fantasy.ToolResultContentTypeText:
 867							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
 868							if !ok {
 869								continue
 870							}
 871							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 872								{
 873									OfText: &anthropic.TextBlockParam{
 874										Text: content.Text,
 875									},
 876								},
 877							}
 878						case fantasy.ToolResultContentTypeMedia:
 879							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
 880							if !ok {
 881								continue
 882							}
 883							contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
 884								{
 885									OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
 886								},
 887							}
 888							if content.Text != "" {
 889								contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
 890									OfText: &anthropic.TextBlockParam{
 891										Text: content.Text,
 892									},
 893								})
 894							}
 895							toolResultBlock.Content = contentBlocks
 896						case fantasy.ToolResultContentTypeError:
 897							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
 898							if !ok {
 899								continue
 900							}
 901							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 902								{
 903									OfText: &anthropic.TextBlockParam{
 904										Text: content.Error.Error(),
 905									},
 906								},
 907							}
 908							toolResultBlock.IsError = param.NewOpt(true)
 909						}
 910						if cacheControl != nil {
 911							toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 912						}
 913						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 914							OfToolResult: &toolResultBlock,
 915						})
 916					}
 917				}
 918			}
 919			if !hasVisibleUserContent(anthropicContent) {
 920				warnings = append(warnings, fantasy.CallWarning{
 921					Type:    fantasy.CallWarningTypeOther,
 922					Message: "dropping empty user message (contains neither user-facing content nor tool results)",
 923				})
 924				continue
 925			}
 926			messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
 927		case fantasy.MessageRoleAssistant:
 928			var anthropicContent []anthropic.ContentBlockParamUnion
 929			for _, msg := range block.Messages {
 930				for i, part := range msg.Content {
 931					isLastPart := i == len(msg.Content)-1
 932					cacheControl := GetCacheControl(part.Options())
 933					if cacheControl == nil && isLastPart {
 934						cacheControl = GetCacheControl(msg.ProviderOptions)
 935					}
 936					switch part.GetType() {
 937					case fantasy.ContentTypeText:
 938						text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 939						if !ok {
 940							continue
 941						}
 942						textBlock := &anthropic.TextBlockParam{
 943							Text: text.Text,
 944						}
 945						if cacheControl != nil {
 946							textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 947						}
 948						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 949							OfText: textBlock,
 950						})
 951					case fantasy.ContentTypeReasoning:
 952						reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
 953						if !ok {
 954							continue
 955						}
 956						if !sendReasoningData {
 957							warnings = append(warnings, fantasy.CallWarning{
 958								Type:    fantasy.CallWarningTypeOther,
 959								Message: "sending reasoning content is disabled for this model",
 960							})
 961							continue
 962						}
 963						reasoningMetadata := GetReasoningMetadata(part.Options())
 964						if reasoningMetadata == nil {
 965							warnings = append(warnings, fantasy.CallWarning{
 966								Type:    fantasy.CallWarningTypeOther,
 967								Message: "unsupported reasoning metadata",
 968							})
 969							continue
 970						}
 971
 972						if reasoningMetadata.Signature != "" {
 973							anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
 974						} else if reasoningMetadata.RedactedData != "" {
 975							anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
 976						} else {
 977							warnings = append(warnings, fantasy.CallWarning{
 978								Type:    fantasy.CallWarningTypeOther,
 979								Message: "unsupported reasoning metadata",
 980							})
 981							continue
 982						}
 983					case fantasy.ContentTypeToolCall:
 984						toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
 985						if !ok {
 986							continue
 987						}
 988						if toolCall.ProviderExecuted {
 989							// Reconstruct server_tool_use block for
 990							// multi-turn round-tripping.
 991							var inputAny any
 992							err := json.Unmarshal([]byte(toolCall.Input), &inputAny)
 993							if err != nil {
 994								continue
 995							}
 996							anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 997								OfServerToolUse: &anthropic.ServerToolUseBlockParam{
 998									ID:    toolCall.ToolCallID,
 999									Name:  anthropic.ServerToolUseBlockParamName(toolCall.ToolName),
1000									Input: inputAny,
1001								},
1002							})
1003							continue
1004						}
1005						var inputMap map[string]any
1006						err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
1007						if err != nil {
1008							continue
1009						}
1010						toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
1011						if cacheControl != nil {
1012							toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
1013						}
1014						anthropicContent = append(anthropicContent, toolUseBlock)
1015					case fantasy.ContentTypeToolResult:
1016						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
1017						if !ok {
1018							continue
1019						}
1020						if result.ProviderExecuted {
1021							// Reconstruct web_search_tool_result block
1022							// with encrypted_content for round-tripping.
1023							searchMeta := &WebSearchResultMetadata{}
1024							if webMeta, ok := result.ProviderOptions[Name]; ok {
1025								if typed, ok := webMeta.(*WebSearchResultMetadata); ok {
1026									searchMeta = typed
1027								}
1028							}
1029							anthropicContent = append(anthropicContent, buildWebSearchToolResultBlock(result.ToolCallID, searchMeta))
1030							continue
1031						}
1032					case fantasy.ContentTypeSource: // Source content from web search results is not a
1033						// recognized Anthropic content block type; skip it.
1034						continue
1035					}
1036				}
1037			}
1038			if !hasVisibleAssistantContent(anthropicContent) {
1039				warnings = append(warnings, fantasy.CallWarning{
1040					Type:    fantasy.CallWarningTypeOther,
1041					Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
1042				})
1043				continue
1044			}
1045			messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
1046		}
1047	}
1048	return systemBlocks, messages, warnings
1049}
1050
1051func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
1052	for _, block := range content {
1053		if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
1054			return true
1055		}
1056	}
1057	return false
1058}
1059
1060func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
1061	for _, block := range content {
1062		if block.OfText != nil || block.OfToolUse != nil || block.OfServerToolUse != nil || block.OfWebSearchToolResult != nil {
1063			return true
1064		}
1065	}
1066	return false
1067}
1068
1069// buildWebSearchToolResultBlock constructs an Anthropic
1070// web_search_tool_result content block from structured metadata.
1071func buildWebSearchToolResultBlock(toolCallID string, searchMeta *WebSearchResultMetadata) anthropic.ContentBlockParamUnion {
1072	resultBlocks := make([]anthropic.WebSearchResultBlockParam, 0, len(searchMeta.Results))
1073	for _, r := range searchMeta.Results {
1074		block := anthropic.WebSearchResultBlockParam{
1075			URL:              r.URL,
1076			Title:            r.Title,
1077			EncryptedContent: r.EncryptedContent,
1078		}
1079		if r.PageAge != "" {
1080			block.PageAge = param.NewOpt(r.PageAge)
1081		}
1082		resultBlocks = append(resultBlocks, block)
1083	}
1084	return anthropic.ContentBlockParamUnion{
1085		OfWebSearchToolResult: &anthropic.WebSearchToolResultBlockParam{
1086			ToolUseID: toolCallID,
1087			Content: anthropic.WebSearchToolResultBlockParamContentUnion{
1088				OfWebSearchToolResultBlockItem: resultBlocks,
1089			},
1090		},
1091	}
1092}
1093
1094func mapFinishReason(finishReason string) fantasy.FinishReason {
1095	switch finishReason {
1096	case "end_turn", "pause_turn", "stop_sequence":
1097		return fantasy.FinishReasonStop
1098	case "max_tokens":
1099		return fantasy.FinishReasonLength
1100	case "tool_use":
1101		return fantasy.FinishReasonToolCalls
1102	default:
1103		return fantasy.FinishReasonUnknown
1104	}
1105}
1106
1107// Generate implements fantasy.LanguageModel.
1108func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
1109	params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
1110	if err != nil {
1111		return nil, err
1112	}
1113	reqOpts := buildRequestOptions(call, rawTools, betaFlags)
1114
1115	response, err := a.client.Messages.New(ctx, *params, reqOpts...)
1116	if err != nil {
1117		return nil, toProviderErr(err)
1118	}
1119
1120	var content []fantasy.Content
1121	for _, block := range response.Content {
1122		switch block.Type {
1123		case "text":
1124			text, ok := block.AsAny().(anthropic.TextBlock)
1125			if !ok {
1126				continue
1127			}
1128			content = append(content, fantasy.TextContent{
1129				Text: text.Text,
1130			})
1131		case "thinking":
1132			reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
1133			if !ok {
1134				continue
1135			}
1136			content = append(content, fantasy.ReasoningContent{
1137				Text: reasoning.Thinking,
1138				ProviderMetadata: fantasy.ProviderMetadata{
1139					Name: &ReasoningOptionMetadata{
1140						Signature: reasoning.Signature,
1141					},
1142				},
1143			})
1144		case "redacted_thinking":
1145			reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
1146			if !ok {
1147				continue
1148			}
1149			content = append(content, fantasy.ReasoningContent{
1150				Text: "",
1151				ProviderMetadata: fantasy.ProviderMetadata{
1152					Name: &ReasoningOptionMetadata{
1153						RedactedData: reasoning.Data,
1154					},
1155				},
1156			})
1157		case "tool_use":
1158			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
1159			if !ok {
1160				continue
1161			}
1162			content = append(content, fantasy.ToolCallContent{
1163				ToolCallID:       toolUse.ID,
1164				ToolName:         toolUse.Name,
1165				Input:            string(toolUse.Input),
1166				ProviderExecuted: false,
1167			})
1168		case "server_tool_use":
1169			serverToolUse, ok := block.AsAny().(anthropic.ServerToolUseBlock)
1170			if !ok {
1171				continue
1172			}
1173			var inputStr string
1174			if b, err := json.Marshal(serverToolUse.Input); err == nil {
1175				inputStr = string(b)
1176			}
1177			content = append(content, fantasy.ToolCallContent{
1178				ToolCallID:       serverToolUse.ID,
1179				ToolName:         string(serverToolUse.Name),
1180				Input:            inputStr,
1181				ProviderExecuted: true,
1182			})
1183		case "web_search_tool_result":
1184			webSearchResult, ok := block.AsAny().(anthropic.WebSearchToolResultBlock)
1185			if !ok {
1186				continue
1187			}
1188			// Extract search results as sources/citations, preserving
1189			// encrypted_content for multi-turn round-tripping.
1190			toolResult := fantasy.ToolResultContent{
1191				ToolCallID:       webSearchResult.ToolUseID,
1192				ToolName:         "web_search",
1193				ProviderExecuted: true,
1194			}
1195			if items := webSearchResult.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1196				var metadataResults []WebSearchResultItem
1197				for _, item := range items {
1198					content = append(content, fantasy.SourceContent{
1199						SourceType: fantasy.SourceTypeURL,
1200						ID:         item.URL,
1201						URL:        item.URL,
1202						Title:      item.Title,
1203					})
1204					metadataResults = append(metadataResults, WebSearchResultItem{
1205						URL:              item.URL,
1206						Title:            item.Title,
1207						EncryptedContent: item.EncryptedContent,
1208						PageAge:          item.PageAge,
1209					})
1210				}
1211				toolResult.ProviderMetadata = fantasy.ProviderMetadata{
1212					Name: &WebSearchResultMetadata{
1213						Results: metadataResults,
1214					},
1215				}
1216			}
1217			content = append(content, toolResult)
1218		}
1219	}
1220
1221	return &fantasy.Response{
1222		Content: content,
1223		Usage: fantasy.Usage{
1224			InputTokens:         response.Usage.InputTokens,
1225			OutputTokens:        response.Usage.OutputTokens,
1226			TotalTokens:         response.Usage.InputTokens + response.Usage.OutputTokens,
1227			CacheCreationTokens: response.Usage.CacheCreationInputTokens,
1228			CacheReadTokens:     response.Usage.CacheReadInputTokens,
1229		},
1230		FinishReason:     mapFinishReason(string(response.StopReason)),
1231		ProviderMetadata: fantasy.ProviderMetadata{},
1232		Warnings:         warnings,
1233	}, nil
1234}
1235
1236// Stream implements fantasy.LanguageModel.
1237func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
1238	params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
1239	if err != nil {
1240		return nil, err
1241	}
1242
1243	reqOpts := buildRequestOptions(call, rawTools, betaFlags)
1244
1245	stream := a.client.Messages.NewStreaming(ctx, *params, reqOpts...)
1246	acc := anthropic.Message{}
1247	return func(yield func(fantasy.StreamPart) bool) {
1248		if len(warnings) > 0 {
1249			if !yield(fantasy.StreamPart{
1250				Type:     fantasy.StreamPartTypeWarnings,
1251				Warnings: warnings,
1252			}) {
1253				return
1254			}
1255		}
1256
1257		for stream.Next() {
1258			chunk := stream.Current()
1259			_ = acc.Accumulate(chunk)
1260			switch chunk.Type {
1261			case "content_block_start":
1262				contentBlockType := chunk.ContentBlock.Type
1263				switch contentBlockType {
1264				case "text":
1265					if !yield(fantasy.StreamPart{
1266						Type: fantasy.StreamPartTypeTextStart,
1267						ID:   fmt.Sprintf("%d", chunk.Index),
1268					}) {
1269						return
1270					}
1271				case "thinking":
1272					if !yield(fantasy.StreamPart{
1273						Type: fantasy.StreamPartTypeReasoningStart,
1274						ID:   fmt.Sprintf("%d", chunk.Index),
1275					}) {
1276						return
1277					}
1278				case "redacted_thinking":
1279					if !yield(fantasy.StreamPart{
1280						Type: fantasy.StreamPartTypeReasoningStart,
1281						ID:   fmt.Sprintf("%d", chunk.Index),
1282						ProviderMetadata: fantasy.ProviderMetadata{
1283							Name: &ReasoningOptionMetadata{
1284								RedactedData: chunk.ContentBlock.Data,
1285							},
1286						},
1287					}) {
1288						return
1289					}
1290				case "tool_use":
1291					if !yield(fantasy.StreamPart{
1292						Type:          fantasy.StreamPartTypeToolInputStart,
1293						ID:            chunk.ContentBlock.ID,
1294						ToolCallName:  chunk.ContentBlock.Name,
1295						ToolCallInput: "",
1296					}) {
1297						return
1298					}
1299				case "server_tool_use":
1300					if !yield(fantasy.StreamPart{
1301						Type:             fantasy.StreamPartTypeToolInputStart,
1302						ID:               chunk.ContentBlock.ID,
1303						ToolCallName:     chunk.ContentBlock.Name,
1304						ToolCallInput:    "",
1305						ProviderExecuted: true,
1306					}) {
1307						return
1308					}
1309				}
1310			case "content_block_stop":
1311				if len(acc.Content)-1 < int(chunk.Index) {
1312					continue
1313				}
1314				contentBlock := acc.Content[int(chunk.Index)]
1315				switch contentBlock.Type {
1316				case "text":
1317					if !yield(fantasy.StreamPart{
1318						Type: fantasy.StreamPartTypeTextEnd,
1319						ID:   fmt.Sprintf("%d", chunk.Index),
1320					}) {
1321						return
1322					}
1323				case "thinking":
1324					if !yield(fantasy.StreamPart{
1325						Type: fantasy.StreamPartTypeReasoningEnd,
1326						ID:   fmt.Sprintf("%d", chunk.Index),
1327					}) {
1328						return
1329					}
1330				case "tool_use":
1331					if !yield(fantasy.StreamPart{
1332						Type: fantasy.StreamPartTypeToolInputEnd,
1333						ID:   contentBlock.ID,
1334					}) {
1335						return
1336					}
1337					if !yield(fantasy.StreamPart{
1338						Type:          fantasy.StreamPartTypeToolCall,
1339						ID:            contentBlock.ID,
1340						ToolCallName:  contentBlock.Name,
1341						ToolCallInput: string(contentBlock.Input),
1342					}) {
1343						return
1344					}
1345				case "server_tool_use":
1346					if !yield(fantasy.StreamPart{
1347						Type:             fantasy.StreamPartTypeToolInputEnd,
1348						ID:               contentBlock.ID,
1349						ProviderExecuted: true,
1350					}) {
1351						return
1352					}
1353					if !yield(fantasy.StreamPart{
1354						Type:             fantasy.StreamPartTypeToolCall,
1355						ID:               contentBlock.ID,
1356						ToolCallName:     contentBlock.Name,
1357						ToolCallInput:    string(contentBlock.Input),
1358						ProviderExecuted: true,
1359					}) {
1360						return
1361					}
1362				case "web_search_tool_result":
1363					// Read search results directly from the ContentBlockUnion
1364					// struct fields instead of using AsAny(). The Anthropic SDK's
1365					// Accumulate re-marshals the content block at content_block_stop,
1366					// which corrupts JSON.raw for inline union types like
1367					// WebSearchToolResultBlockContentUnion. The struct fields
1368					// themselves remain correctly populated from content_block_start.
1369					var metadataResults []WebSearchResultItem
1370					var providerMeta fantasy.ProviderMetadata
1371					if items := contentBlock.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1372						for _, item := range items {
1373							if !yield(fantasy.StreamPart{
1374								Type:       fantasy.StreamPartTypeSource,
1375								ID:         item.URL,
1376								SourceType: fantasy.SourceTypeURL,
1377								URL:        item.URL,
1378								Title:      item.Title,
1379							}) {
1380								return
1381							}
1382							metadataResults = append(metadataResults, WebSearchResultItem{
1383								URL:              item.URL,
1384								Title:            item.Title,
1385								EncryptedContent: item.EncryptedContent,
1386								PageAge:          item.PageAge,
1387							})
1388						}
1389					}
1390					if len(metadataResults) > 0 {
1391						providerMeta = fantasy.ProviderMetadata{
1392							Name: &WebSearchResultMetadata{
1393								Results: metadataResults,
1394							},
1395						}
1396					}
1397					if !yield(fantasy.StreamPart{
1398						Type:             fantasy.StreamPartTypeToolResult,
1399						ID:               contentBlock.ToolUseID,
1400						ToolCallName:     "web_search",
1401						ProviderExecuted: true,
1402						ProviderMetadata: providerMeta,
1403					}) {
1404						return
1405					}
1406				}
1407			case "content_block_delta":
1408				switch chunk.Delta.Type {
1409				case "text_delta":
1410					if !yield(fantasy.StreamPart{
1411						Type:  fantasy.StreamPartTypeTextDelta,
1412						ID:    fmt.Sprintf("%d", chunk.Index),
1413						Delta: chunk.Delta.Text,
1414					}) {
1415						return
1416					}
1417				case "thinking_delta":
1418					if !yield(fantasy.StreamPart{
1419						Type:  fantasy.StreamPartTypeReasoningDelta,
1420						ID:    fmt.Sprintf("%d", chunk.Index),
1421						Delta: chunk.Delta.Thinking,
1422					}) {
1423						return
1424					}
1425				case "signature_delta":
1426					if !yield(fantasy.StreamPart{
1427						Type: fantasy.StreamPartTypeReasoningDelta,
1428						ID:   fmt.Sprintf("%d", chunk.Index),
1429						ProviderMetadata: fantasy.ProviderMetadata{
1430							Name: &ReasoningOptionMetadata{
1431								Signature: chunk.Delta.Signature,
1432							},
1433						},
1434					}) {
1435						return
1436					}
1437				case "input_json_delta":
1438					if len(acc.Content)-1 < int(chunk.Index) {
1439						continue
1440					}
1441					contentBlock := acc.Content[int(chunk.Index)]
1442					if !yield(fantasy.StreamPart{
1443						Type:          fantasy.StreamPartTypeToolInputDelta,
1444						ID:            contentBlock.ID,
1445						ToolCallInput: chunk.Delta.PartialJSON,
1446					}) {
1447						return
1448					}
1449				}
1450			case "message_stop":
1451			}
1452		}
1453
1454		err := stream.Err()
1455		if err == nil || errors.Is(err, io.EOF) {
1456			yield(fantasy.StreamPart{
1457				Type:         fantasy.StreamPartTypeFinish,
1458				ID:           acc.ID,
1459				FinishReason: mapFinishReason(string(acc.StopReason)),
1460				Usage: fantasy.Usage{
1461					InputTokens:         acc.Usage.InputTokens,
1462					OutputTokens:        acc.Usage.OutputTokens,
1463					TotalTokens:         acc.Usage.InputTokens + acc.Usage.OutputTokens,
1464					CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
1465					CacheReadTokens:     acc.Usage.CacheReadInputTokens,
1466				},
1467				ProviderMetadata: fantasy.ProviderMetadata{},
1468			})
1469			return
1470		} else { //nolint: revive
1471			yield(fantasy.StreamPart{
1472				Type:  fantasy.StreamPartTypeError,
1473				Error: toProviderErr(err),
1474			})
1475			return
1476		}
1477	}, nil
1478}
1479
1480// GenerateObject implements fantasy.LanguageModel.
1481func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1482	switch a.options.objectMode {
1483	case fantasy.ObjectModeText:
1484		return object.GenerateWithText(ctx, a, call)
1485	default:
1486		return object.GenerateWithTool(ctx, a, call)
1487	}
1488}
1489
1490// StreamObject implements fantasy.LanguageModel.
1491func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1492	switch a.options.objectMode {
1493	case fantasy.ObjectModeText:
1494		return object.StreamWithText(ctx, a, call)
1495	default:
1496		return object.StreamWithTool(ctx, a, call)
1497	}
1498}