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