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