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							switch {
 835							case strings.HasPrefix(file.MediaType, "image/"):
 836								base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
 837								imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
 838								if cacheControl != nil {
 839									imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
 840								}
 841								anthropicContent = append(anthropicContent, imageBlock)
 842							case file.MediaType == "application/pdf":
 843								base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
 844								docBlock := anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{
 845									Data: base64Encoded,
 846								})
 847								if cacheControl != nil {
 848									docBlock.OfDocument.CacheControl = anthropic.NewCacheControlEphemeralParam()
 849								}
 850								anthropicContent = append(anthropicContent, docBlock)
 851							case strings.HasPrefix(file.MediaType, "text/"):
 852								documentBlock := anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{
 853									Data: string(file.Data),
 854								})
 855								if cacheControl != nil {
 856									documentBlock.OfDocument.CacheControl = anthropic.NewCacheControlEphemeralParam()
 857								}
 858								anthropicContent = append(anthropicContent, documentBlock)
 859							}
 860						}
 861					}
 862				} else if msg.Role == fantasy.MessageRoleTool {
 863					for i, part := range msg.Content {
 864						isLastPart := i == len(msg.Content)-1
 865						cacheControl := GetCacheControl(part.Options())
 866						if cacheControl == nil && isLastPart {
 867							cacheControl = GetCacheControl(msg.ProviderOptions)
 868						}
 869						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
 870						if !ok {
 871							continue
 872						}
 873						toolResultBlock := anthropic.ToolResultBlockParam{
 874							ToolUseID: result.ToolCallID,
 875						}
 876						switch result.Output.GetType() {
 877						case fantasy.ToolResultContentTypeText:
 878							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
 879							if !ok {
 880								continue
 881							}
 882							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 883								{
 884									OfText: &anthropic.TextBlockParam{
 885										Text: content.Text,
 886									},
 887								},
 888							}
 889						case fantasy.ToolResultContentTypeMedia:
 890							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
 891							if !ok {
 892								continue
 893							}
 894							contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
 895								{
 896									OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
 897								},
 898							}
 899							if content.Text != "" {
 900								contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
 901									OfText: &anthropic.TextBlockParam{
 902										Text: content.Text,
 903									},
 904								})
 905							}
 906							toolResultBlock.Content = contentBlocks
 907						case fantasy.ToolResultContentTypeError:
 908							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
 909							if !ok {
 910								continue
 911							}
 912							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 913								{
 914									OfText: &anthropic.TextBlockParam{
 915										Text: content.Error.Error(),
 916									},
 917								},
 918							}
 919							toolResultBlock.IsError = param.NewOpt(true)
 920						}
 921						if cacheControl != nil {
 922							toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 923						}
 924						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 925							OfToolResult: &toolResultBlock,
 926						})
 927					}
 928				}
 929			}
 930			if !hasVisibleUserContent(anthropicContent) {
 931				warnings = append(warnings, fantasy.CallWarning{
 932					Type:    fantasy.CallWarningTypeOther,
 933					Message: "dropping empty user message (contains neither user-facing content nor tool results)",
 934				})
 935				continue
 936			}
 937			messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
 938		case fantasy.MessageRoleAssistant:
 939			var anthropicContent []anthropic.ContentBlockParamUnion
 940			for _, msg := range block.Messages {
 941				for i, part := range msg.Content {
 942					isLastPart := i == len(msg.Content)-1
 943					cacheControl := GetCacheControl(part.Options())
 944					if cacheControl == nil && isLastPart {
 945						cacheControl = GetCacheControl(msg.ProviderOptions)
 946					}
 947					switch part.GetType() {
 948					case fantasy.ContentTypeText:
 949						text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 950						if !ok {
 951							continue
 952						}
 953						textBlock := &anthropic.TextBlockParam{
 954							Text: text.Text,
 955						}
 956						if cacheControl != nil {
 957							textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 958						}
 959						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 960							OfText: textBlock,
 961						})
 962					case fantasy.ContentTypeReasoning:
 963						reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
 964						if !ok {
 965							continue
 966						}
 967						if !sendReasoningData {
 968							warnings = append(warnings, fantasy.CallWarning{
 969								Type:    fantasy.CallWarningTypeOther,
 970								Message: "sending reasoning content is disabled for this model",
 971							})
 972							continue
 973						}
 974						reasoningMetadata := GetReasoningMetadata(part.Options())
 975						if reasoningMetadata == nil {
 976							warnings = append(warnings, fantasy.CallWarning{
 977								Type:    fantasy.CallWarningTypeOther,
 978								Message: "unsupported reasoning metadata",
 979							})
 980							continue
 981						}
 982
 983						if reasoningMetadata.Signature != "" {
 984							anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
 985						} else if reasoningMetadata.RedactedData != "" {
 986							anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
 987						} else {
 988							warnings = append(warnings, fantasy.CallWarning{
 989								Type:    fantasy.CallWarningTypeOther,
 990								Message: "unsupported reasoning metadata",
 991							})
 992							continue
 993						}
 994					case fantasy.ContentTypeToolCall:
 995						toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
 996						if !ok {
 997							continue
 998						}
 999						if toolCall.ProviderExecuted {
1000							// Reconstruct server_tool_use block for
1001							// multi-turn round-tripping.
1002							var inputAny any
1003							err := json.Unmarshal([]byte(toolCall.Input), &inputAny)
1004							if err != nil {
1005								continue
1006							}
1007							anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
1008								OfServerToolUse: &anthropic.ServerToolUseBlockParam{
1009									ID:    toolCall.ToolCallID,
1010									Name:  anthropic.ServerToolUseBlockParamName(toolCall.ToolName),
1011									Input: inputAny,
1012								},
1013							})
1014							continue
1015						}
1016						var inputMap map[string]any
1017						err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
1018						if err != nil {
1019							continue
1020						}
1021						toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
1022						if cacheControl != nil {
1023							toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
1024						}
1025						anthropicContent = append(anthropicContent, toolUseBlock)
1026					case fantasy.ContentTypeToolResult:
1027						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
1028						if !ok {
1029							continue
1030						}
1031						if result.ProviderExecuted {
1032							// Reconstruct web_search_tool_result block
1033							// with encrypted_content for round-tripping.
1034							searchMeta := &WebSearchResultMetadata{}
1035							if webMeta, ok := result.ProviderOptions[Name]; ok {
1036								if typed, ok := webMeta.(*WebSearchResultMetadata); ok {
1037									searchMeta = typed
1038								}
1039							}
1040							anthropicContent = append(anthropicContent, buildWebSearchToolResultBlock(result.ToolCallID, searchMeta))
1041							continue
1042						}
1043					case fantasy.ContentTypeSource: // Source content from web search results is not a
1044						// recognized Anthropic content block type; skip it.
1045						continue
1046					}
1047				}
1048			}
1049			if !hasVisibleAssistantContent(anthropicContent) {
1050				warnings = append(warnings, fantasy.CallWarning{
1051					Type:    fantasy.CallWarningTypeOther,
1052					Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
1053				})
1054				continue
1055			}
1056			messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
1057		}
1058	}
1059	return systemBlocks, messages, warnings
1060}
1061
1062func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
1063	for _, block := range content {
1064		if block.OfText != nil || block.OfImage != nil || block.OfDocument != nil || block.OfToolResult != nil {
1065			return true
1066		}
1067	}
1068	return false
1069}
1070
1071func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
1072	for _, block := range content {
1073		if block.OfText != nil || block.OfToolUse != nil || block.OfServerToolUse != nil || block.OfWebSearchToolResult != nil {
1074			return true
1075		}
1076	}
1077	return false
1078}
1079
1080// buildWebSearchToolResultBlock constructs an Anthropic
1081// web_search_tool_result content block from structured metadata.
1082func buildWebSearchToolResultBlock(toolCallID string, searchMeta *WebSearchResultMetadata) anthropic.ContentBlockParamUnion {
1083	resultBlocks := make([]anthropic.WebSearchResultBlockParam, 0, len(searchMeta.Results))
1084	for _, r := range searchMeta.Results {
1085		block := anthropic.WebSearchResultBlockParam{
1086			URL:              r.URL,
1087			Title:            r.Title,
1088			EncryptedContent: r.EncryptedContent,
1089		}
1090		if r.PageAge != "" {
1091			block.PageAge = param.NewOpt(r.PageAge)
1092		}
1093		resultBlocks = append(resultBlocks, block)
1094	}
1095	return anthropic.ContentBlockParamUnion{
1096		OfWebSearchToolResult: &anthropic.WebSearchToolResultBlockParam{
1097			ToolUseID: toolCallID,
1098			Content: anthropic.WebSearchToolResultBlockParamContentUnion{
1099				OfWebSearchToolResultBlockItem: resultBlocks,
1100			},
1101		},
1102	}
1103}
1104
1105func mapFinishReason(finishReason string) fantasy.FinishReason {
1106	switch finishReason {
1107	case "end_turn", "pause_turn", "stop_sequence":
1108		return fantasy.FinishReasonStop
1109	case "max_tokens":
1110		return fantasy.FinishReasonLength
1111	case "tool_use":
1112		return fantasy.FinishReasonToolCalls
1113	default:
1114		return fantasy.FinishReasonUnknown
1115	}
1116}
1117
1118// Generate implements fantasy.LanguageModel.
1119func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
1120	params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
1121	if err != nil {
1122		return nil, err
1123	}
1124	reqOpts := buildRequestOptions(call, rawTools, betaFlags)
1125
1126	response, err := a.client.Messages.New(ctx, *params, reqOpts...)
1127	if err != nil {
1128		return nil, toProviderErr(err)
1129	}
1130
1131	var content []fantasy.Content
1132	for _, block := range response.Content {
1133		switch block.Type {
1134		case "text":
1135			text, ok := block.AsAny().(anthropic.TextBlock)
1136			if !ok {
1137				continue
1138			}
1139			content = append(content, fantasy.TextContent{
1140				Text: text.Text,
1141			})
1142		case "thinking":
1143			reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
1144			if !ok {
1145				continue
1146			}
1147			content = append(content, fantasy.ReasoningContent{
1148				Text: reasoning.Thinking,
1149				ProviderMetadata: fantasy.ProviderMetadata{
1150					Name: &ReasoningOptionMetadata{
1151						Signature: reasoning.Signature,
1152					},
1153				},
1154			})
1155		case "redacted_thinking":
1156			reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
1157			if !ok {
1158				continue
1159			}
1160			content = append(content, fantasy.ReasoningContent{
1161				Text: "",
1162				ProviderMetadata: fantasy.ProviderMetadata{
1163					Name: &ReasoningOptionMetadata{
1164						RedactedData: reasoning.Data,
1165					},
1166				},
1167			})
1168		case "tool_use":
1169			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
1170			if !ok {
1171				continue
1172			}
1173			content = append(content, fantasy.ToolCallContent{
1174				ToolCallID:       toolUse.ID,
1175				ToolName:         toolUse.Name,
1176				Input:            string(toolUse.Input),
1177				ProviderExecuted: false,
1178			})
1179		case "server_tool_use":
1180			serverToolUse, ok := block.AsAny().(anthropic.ServerToolUseBlock)
1181			if !ok {
1182				continue
1183			}
1184			var inputStr string
1185			if b, err := json.Marshal(serverToolUse.Input); err == nil {
1186				inputStr = string(b)
1187			}
1188			content = append(content, fantasy.ToolCallContent{
1189				ToolCallID:       serverToolUse.ID,
1190				ToolName:         string(serverToolUse.Name),
1191				Input:            inputStr,
1192				ProviderExecuted: true,
1193			})
1194		case "web_search_tool_result":
1195			webSearchResult, ok := block.AsAny().(anthropic.WebSearchToolResultBlock)
1196			if !ok {
1197				continue
1198			}
1199			// Extract search results as sources/citations, preserving
1200			// encrypted_content for multi-turn round-tripping.
1201			toolResult := fantasy.ToolResultContent{
1202				ToolCallID:       webSearchResult.ToolUseID,
1203				ToolName:         "web_search",
1204				ProviderExecuted: true,
1205			}
1206			if items := webSearchResult.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1207				var metadataResults []WebSearchResultItem
1208				for _, item := range items {
1209					content = append(content, fantasy.SourceContent{
1210						SourceType: fantasy.SourceTypeURL,
1211						ID:         item.URL,
1212						URL:        item.URL,
1213						Title:      item.Title,
1214					})
1215					metadataResults = append(metadataResults, WebSearchResultItem{
1216						URL:              item.URL,
1217						Title:            item.Title,
1218						EncryptedContent: item.EncryptedContent,
1219						PageAge:          item.PageAge,
1220					})
1221				}
1222				toolResult.ProviderMetadata = fantasy.ProviderMetadata{
1223					Name: &WebSearchResultMetadata{
1224						Results: metadataResults,
1225					},
1226				}
1227			}
1228			content = append(content, toolResult)
1229		}
1230	}
1231
1232	return &fantasy.Response{
1233		Content: content,
1234		Usage: fantasy.Usage{
1235			InputTokens:         response.Usage.InputTokens,
1236			OutputTokens:        response.Usage.OutputTokens,
1237			TotalTokens:         response.Usage.InputTokens + response.Usage.OutputTokens,
1238			CacheCreationTokens: response.Usage.CacheCreationInputTokens,
1239			CacheReadTokens:     response.Usage.CacheReadInputTokens,
1240		},
1241		FinishReason:     mapFinishReason(string(response.StopReason)),
1242		ProviderMetadata: fantasy.ProviderMetadata{},
1243		Warnings:         warnings,
1244	}, nil
1245}
1246
1247// Stream implements fantasy.LanguageModel.
1248func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
1249	params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
1250	if err != nil {
1251		return nil, err
1252	}
1253
1254	reqOpts := buildRequestOptions(call, rawTools, betaFlags)
1255
1256	stream := a.client.Messages.NewStreaming(ctx, *params, reqOpts...)
1257	acc := anthropic.Message{}
1258	return func(yield func(fantasy.StreamPart) bool) {
1259		if len(warnings) > 0 {
1260			if !yield(fantasy.StreamPart{
1261				Type:     fantasy.StreamPartTypeWarnings,
1262				Warnings: warnings,
1263			}) {
1264				return
1265			}
1266		}
1267
1268		for stream.Next() {
1269			chunk := stream.Current()
1270			_ = acc.Accumulate(chunk)
1271			switch chunk.Type {
1272			case "content_block_start":
1273				contentBlockType := chunk.ContentBlock.Type
1274				switch contentBlockType {
1275				case "text":
1276					if !yield(fantasy.StreamPart{
1277						Type: fantasy.StreamPartTypeTextStart,
1278						ID:   fmt.Sprintf("%d", chunk.Index),
1279					}) {
1280						return
1281					}
1282				case "thinking":
1283					if !yield(fantasy.StreamPart{
1284						Type: fantasy.StreamPartTypeReasoningStart,
1285						ID:   fmt.Sprintf("%d", chunk.Index),
1286					}) {
1287						return
1288					}
1289				case "redacted_thinking":
1290					if !yield(fantasy.StreamPart{
1291						Type: fantasy.StreamPartTypeReasoningStart,
1292						ID:   fmt.Sprintf("%d", chunk.Index),
1293						ProviderMetadata: fantasy.ProviderMetadata{
1294							Name: &ReasoningOptionMetadata{
1295								RedactedData: chunk.ContentBlock.Data,
1296							},
1297						},
1298					}) {
1299						return
1300					}
1301				case "tool_use":
1302					if !yield(fantasy.StreamPart{
1303						Type:          fantasy.StreamPartTypeToolInputStart,
1304						ID:            chunk.ContentBlock.ID,
1305						ToolCallName:  chunk.ContentBlock.Name,
1306						ToolCallInput: "",
1307					}) {
1308						return
1309					}
1310				case "server_tool_use":
1311					if !yield(fantasy.StreamPart{
1312						Type:             fantasy.StreamPartTypeToolInputStart,
1313						ID:               chunk.ContentBlock.ID,
1314						ToolCallName:     chunk.ContentBlock.Name,
1315						ToolCallInput:    "",
1316						ProviderExecuted: true,
1317					}) {
1318						return
1319					}
1320				}
1321			case "content_block_stop":
1322				if len(acc.Content)-1 < int(chunk.Index) {
1323					continue
1324				}
1325				contentBlock := acc.Content[int(chunk.Index)]
1326				switch contentBlock.Type {
1327				case "text":
1328					if !yield(fantasy.StreamPart{
1329						Type: fantasy.StreamPartTypeTextEnd,
1330						ID:   fmt.Sprintf("%d", chunk.Index),
1331					}) {
1332						return
1333					}
1334				case "thinking":
1335					if !yield(fantasy.StreamPart{
1336						Type: fantasy.StreamPartTypeReasoningEnd,
1337						ID:   fmt.Sprintf("%d", chunk.Index),
1338					}) {
1339						return
1340					}
1341				case "tool_use":
1342					if !yield(fantasy.StreamPart{
1343						Type: fantasy.StreamPartTypeToolInputEnd,
1344						ID:   contentBlock.ID,
1345					}) {
1346						return
1347					}
1348					if !yield(fantasy.StreamPart{
1349						Type:          fantasy.StreamPartTypeToolCall,
1350						ID:            contentBlock.ID,
1351						ToolCallName:  contentBlock.Name,
1352						ToolCallInput: string(contentBlock.Input),
1353					}) {
1354						return
1355					}
1356				case "server_tool_use":
1357					if !yield(fantasy.StreamPart{
1358						Type:             fantasy.StreamPartTypeToolInputEnd,
1359						ID:               contentBlock.ID,
1360						ProviderExecuted: true,
1361					}) {
1362						return
1363					}
1364					if !yield(fantasy.StreamPart{
1365						Type:             fantasy.StreamPartTypeToolCall,
1366						ID:               contentBlock.ID,
1367						ToolCallName:     contentBlock.Name,
1368						ToolCallInput:    string(contentBlock.Input),
1369						ProviderExecuted: true,
1370					}) {
1371						return
1372					}
1373				case "web_search_tool_result":
1374					// Read search results directly from the ContentBlockUnion
1375					// struct fields instead of using AsAny(). The Anthropic SDK's
1376					// Accumulate re-marshals the content block at content_block_stop,
1377					// which corrupts JSON.raw for inline union types like
1378					// WebSearchToolResultBlockContentUnion. The struct fields
1379					// themselves remain correctly populated from content_block_start.
1380					var metadataResults []WebSearchResultItem
1381					var providerMeta fantasy.ProviderMetadata
1382					if items := contentBlock.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1383						for _, item := range items {
1384							if !yield(fantasy.StreamPart{
1385								Type:       fantasy.StreamPartTypeSource,
1386								ID:         item.URL,
1387								SourceType: fantasy.SourceTypeURL,
1388								URL:        item.URL,
1389								Title:      item.Title,
1390							}) {
1391								return
1392							}
1393							metadataResults = append(metadataResults, WebSearchResultItem{
1394								URL:              item.URL,
1395								Title:            item.Title,
1396								EncryptedContent: item.EncryptedContent,
1397								PageAge:          item.PageAge,
1398							})
1399						}
1400					}
1401					if len(metadataResults) > 0 {
1402						providerMeta = fantasy.ProviderMetadata{
1403							Name: &WebSearchResultMetadata{
1404								Results: metadataResults,
1405							},
1406						}
1407					}
1408					if !yield(fantasy.StreamPart{
1409						Type:             fantasy.StreamPartTypeToolResult,
1410						ID:               contentBlock.ToolUseID,
1411						ToolCallName:     "web_search",
1412						ProviderExecuted: true,
1413						ProviderMetadata: providerMeta,
1414					}) {
1415						return
1416					}
1417				}
1418			case "content_block_delta":
1419				switch chunk.Delta.Type {
1420				case "text_delta":
1421					if !yield(fantasy.StreamPart{
1422						Type:  fantasy.StreamPartTypeTextDelta,
1423						ID:    fmt.Sprintf("%d", chunk.Index),
1424						Delta: chunk.Delta.Text,
1425					}) {
1426						return
1427					}
1428				case "thinking_delta":
1429					if !yield(fantasy.StreamPart{
1430						Type:  fantasy.StreamPartTypeReasoningDelta,
1431						ID:    fmt.Sprintf("%d", chunk.Index),
1432						Delta: chunk.Delta.Thinking,
1433					}) {
1434						return
1435					}
1436				case "signature_delta":
1437					if !yield(fantasy.StreamPart{
1438						Type: fantasy.StreamPartTypeReasoningDelta,
1439						ID:   fmt.Sprintf("%d", chunk.Index),
1440						ProviderMetadata: fantasy.ProviderMetadata{
1441							Name: &ReasoningOptionMetadata{
1442								Signature: chunk.Delta.Signature,
1443							},
1444						},
1445					}) {
1446						return
1447					}
1448				case "input_json_delta":
1449					if len(acc.Content)-1 < int(chunk.Index) {
1450						continue
1451					}
1452					contentBlock := acc.Content[int(chunk.Index)]
1453					if !yield(fantasy.StreamPart{
1454						Type:          fantasy.StreamPartTypeToolInputDelta,
1455						ID:            contentBlock.ID,
1456						ToolCallInput: chunk.Delta.PartialJSON,
1457					}) {
1458						return
1459					}
1460				}
1461			case "message_stop":
1462			}
1463		}
1464
1465		err := stream.Err()
1466		if err == nil || errors.Is(err, io.EOF) {
1467			yield(fantasy.StreamPart{
1468				Type:         fantasy.StreamPartTypeFinish,
1469				ID:           acc.ID,
1470				FinishReason: mapFinishReason(string(acc.StopReason)),
1471				Usage: fantasy.Usage{
1472					InputTokens:         acc.Usage.InputTokens,
1473					OutputTokens:        acc.Usage.OutputTokens,
1474					TotalTokens:         acc.Usage.InputTokens + acc.Usage.OutputTokens,
1475					CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
1476					CacheReadTokens:     acc.Usage.CacheReadInputTokens,
1477				},
1478				ProviderMetadata: fantasy.ProviderMetadata{},
1479			})
1480			return
1481		} else { //nolint: revive
1482			yield(fantasy.StreamPart{
1483				Type:  fantasy.StreamPartTypeError,
1484				Error: toProviderErr(err),
1485			})
1486			return
1487		}
1488	}, nil
1489}
1490
1491// GenerateObject implements fantasy.LanguageModel.
1492func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1493	switch a.options.objectMode {
1494	case fantasy.ObjectModeText:
1495		return object.GenerateWithText(ctx, a, call)
1496	default:
1497		return object.GenerateWithTool(ctx, a, call)
1498	}
1499}
1500
1501// StreamObject implements fantasy.LanguageModel.
1502func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1503	switch a.options.objectMode {
1504	case fantasy.ObjectModeText:
1505		return object.StreamWithText(ctx, a, call)
1506	default:
1507		return object.StreamWithTool(ctx, a, call)
1508	}
1509}