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