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	"strings"
  14
  15	"charm.land/fantasy"
  16	"charm.land/fantasy/object"
  17	"charm.land/fantasy/providers/internal/httpheaders"
  18	"github.com/aws/aws-sdk-go-v2/config"
  19	"github.com/charmbracelet/anthropic-sdk-go"
  20	"github.com/charmbracelet/anthropic-sdk-go/bedrock"
  21	"github.com/charmbracelet/anthropic-sdk-go/option"
  22	"github.com/charmbracelet/anthropic-sdk-go/packages/param"
  23	"github.com/charmbracelet/anthropic-sdk-go/vertex"
  24	"golang.org/x/oauth2/google"
  25)
  26
  27const (
  28	// Name is the name of the Anthropic provider.
  29	Name = "anthropic"
  30	// DefaultURL is the default URL for the Anthropic API.
  31	DefaultURL = "https://api.anthropic.com"
  32)
  33
  34type options struct {
  35	baseURL   string
  36	apiKey    string
  37	name      string
  38	headers   map[string]string
  39	userAgent string
  40	client    option.HTTPClient
  41
  42	vertexProject  string
  43	vertexLocation string
  44	skipAuth       bool
  45
  46	useBedrock bool
  47
  48	objectMode fantasy.ObjectMode
  49}
  50
  51type provider struct {
  52	options options
  53}
  54
  55// Option defines a function that configures Anthropic provider options.
  56type Option = func(*options)
  57
  58// New creates a new Anthropic provider with the given options.
  59func New(opts ...Option) (fantasy.Provider, error) {
  60	providerOptions := options{
  61		headers:    map[string]string{},
  62		objectMode: fantasy.ObjectModeAuto,
  63	}
  64	for _, o := range opts {
  65		o(&providerOptions)
  66	}
  67
  68	providerOptions.baseURL = cmp.Or(providerOptions.baseURL, DefaultURL)
  69	providerOptions.name = cmp.Or(providerOptions.name, Name)
  70	return &provider{options: providerOptions}, nil
  71}
  72
  73// WithBaseURL sets the base URL for the Anthropic provider.
  74func WithBaseURL(baseURL string) Option {
  75	return func(o *options) {
  76		o.baseURL = baseURL
  77	}
  78}
  79
  80// WithAPIKey sets the API key for the Anthropic provider.
  81func WithAPIKey(apiKey string) Option {
  82	return func(o *options) {
  83		o.apiKey = apiKey
  84	}
  85}
  86
  87// WithVertex configures the Anthropic provider to use Vertex AI.
  88func WithVertex(project, location string) Option {
  89	return func(o *options) {
  90		o.vertexProject = project
  91		o.vertexLocation = location
  92	}
  93}
  94
  95// WithSkipAuth configures whether to skip authentication for the Anthropic provider.
  96func WithSkipAuth(skip bool) Option {
  97	return func(o *options) {
  98		o.skipAuth = skip
  99	}
 100}
 101
 102// WithBedrock configures the Anthropic provider to use AWS Bedrock.
 103func WithBedrock() Option {
 104	return func(o *options) {
 105		o.useBedrock = true
 106	}
 107}
 108
 109// WithName sets the name for the Anthropic provider.
 110func WithName(name string) Option {
 111	return func(o *options) {
 112		o.name = name
 113	}
 114}
 115
 116// WithHeaders sets the headers for the Anthropic provider.
 117func WithHeaders(headers map[string]string) Option {
 118	return func(o *options) {
 119		maps.Copy(o.headers, headers)
 120	}
 121}
 122
 123// WithHTTPClient sets the HTTP client for the Anthropic provider.
 124func WithHTTPClient(client option.HTTPClient) Option {
 125	return func(o *options) {
 126		o.client = client
 127	}
 128}
 129
 130// WithUserAgent sets an explicit User-Agent header, overriding the default and any
 131// value set via WithHeaders.
 132func WithUserAgent(ua string) Option {
 133	return func(o *options) {
 134		o.userAgent = ua
 135	}
 136}
 137
 138// WithObjectMode sets the object generation mode.
 139func WithObjectMode(om fantasy.ObjectMode) Option {
 140	return func(o *options) {
 141		// not supported
 142		if om == fantasy.ObjectModeJSON {
 143			om = fantasy.ObjectModeAuto
 144		}
 145		o.objectMode = om
 146	}
 147}
 148
 149func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.LanguageModel, error) {
 150	clientOptions := make([]option.RequestOption, 0, 5+len(a.options.headers))
 151	clientOptions = append(clientOptions, option.WithMaxRetries(0))
 152
 153	if a.options.apiKey != "" && !a.options.useBedrock {
 154		clientOptions = append(clientOptions, option.WithAPIKey(a.options.apiKey))
 155	}
 156	if a.options.baseURL != "" {
 157		clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL))
 158	}
 159	defaultUA := httpheaders.DefaultUserAgent(fantasy.Version)
 160	resolved := httpheaders.ResolveHeaders(a.options.headers, a.options.userAgent, defaultUA)
 161	for key, value := range resolved {
 162		clientOptions = append(clientOptions, option.WithHeader(key, value))
 163	}
 164	if a.options.client != nil {
 165		clientOptions = append(clientOptions, option.WithHTTPClient(a.options.client))
 166	}
 167	if a.options.vertexProject != "" && a.options.vertexLocation != "" {
 168		var credentials *google.Credentials
 169		if a.options.skipAuth {
 170			credentials = &google.Credentials{TokenSource: &googleDummyTokenSource{}}
 171		} else {
 172			var err error
 173			credentials, err = google.FindDefaultCredentials(ctx)
 174			if err != nil {
 175				return nil, err
 176			}
 177		}
 178
 179		clientOptions = append(
 180			clientOptions,
 181			vertex.WithCredentials(
 182				ctx,
 183				a.options.vertexLocation,
 184				a.options.vertexProject,
 185				credentials,
 186			),
 187		)
 188	}
 189	if a.options.useBedrock {
 190		modelID = bedrockPrefixModelWithRegion(modelID)
 191
 192		if a.options.skipAuth || a.options.apiKey != "" {
 193			clientOptions = append(
 194				clientOptions,
 195				bedrock.WithConfig(bedrockBasicAuthConfig(a.options.apiKey)),
 196			)
 197		} else {
 198			if cfg, err := config.LoadDefaultConfig(ctx); err == nil {
 199				clientOptions = append(
 200					clientOptions,
 201					bedrock.WithConfig(cfg),
 202				)
 203			}
 204		}
 205	}
 206	return languageModel{
 207		modelID:  modelID,
 208		provider: a.options.name,
 209		options:  a.options,
 210		client:   anthropic.NewClient(clientOptions...),
 211	}, nil
 212}
 213
 214type languageModel struct {
 215	provider string
 216	modelID  string
 217	client   anthropic.Client
 218	options  options
 219}
 220
 221// Model implements fantasy.LanguageModel.
 222func (a languageModel) Model() string {
 223	return a.modelID
 224}
 225
 226// Provider implements fantasy.LanguageModel.
 227func (a languageModel) Provider() string {
 228	return a.provider
 229}
 230
 231func (a languageModel) prepareParams(call fantasy.Call) (*anthropic.MessageNewParams, []fantasy.CallWarning, error) {
 232	params := &anthropic.MessageNewParams{}
 233	providerOptions := &ProviderOptions{}
 234	if v, ok := call.ProviderOptions[Name]; ok {
 235		providerOptions, ok = v.(*ProviderOptions)
 236		if !ok {
 237			return nil, nil, &fantasy.Error{Title: "invalid argument", Message: "anthropic provider options should be *anthropic.ProviderOptions"}
 238		}
 239	}
 240	sendReasoning := true
 241	if providerOptions.SendReasoning != nil {
 242		sendReasoning = *providerOptions.SendReasoning
 243	}
 244	systemBlocks, messages, warnings := toPrompt(call.Prompt, sendReasoning)
 245
 246	if call.FrequencyPenalty != nil {
 247		warnings = append(warnings, fantasy.CallWarning{
 248			Type:    fantasy.CallWarningTypeUnsupportedSetting,
 249			Setting: "FrequencyPenalty",
 250		})
 251	}
 252	if call.PresencePenalty != nil {
 253		warnings = append(warnings, fantasy.CallWarning{
 254			Type:    fantasy.CallWarningTypeUnsupportedSetting,
 255			Setting: "PresencePenalty",
 256		})
 257	}
 258
 259	params.System = systemBlocks
 260	params.Messages = messages
 261	params.Model = anthropic.Model(a.modelID)
 262	params.MaxTokens = 4096
 263
 264	if call.MaxOutputTokens != nil {
 265		params.MaxTokens = *call.MaxOutputTokens
 266	}
 267
 268	if call.Temperature != nil {
 269		params.Temperature = param.NewOpt(*call.Temperature)
 270	}
 271	if call.TopK != nil {
 272		params.TopK = param.NewOpt(*call.TopK)
 273	}
 274	if call.TopP != nil {
 275		params.TopP = param.NewOpt(*call.TopP)
 276	}
 277
 278	switch {
 279	case providerOptions.Effort != nil:
 280		effort := *providerOptions.Effort
 281		params.OutputConfig = anthropic.OutputConfigParam{
 282			Effort: anthropic.OutputConfigEffort(effort),
 283		}
 284		adaptive := anthropic.NewThinkingConfigAdaptiveParam()
 285		params.Thinking.OfAdaptive = &adaptive
 286	case providerOptions.Thinking != nil:
 287		if providerOptions.Thinking.BudgetTokens == 0 {
 288			return nil, nil, &fantasy.Error{Title: "no budget", Message: "thinking requires budget"}
 289		}
 290		params.Thinking = anthropic.ThinkingConfigParamOfEnabled(providerOptions.Thinking.BudgetTokens)
 291		if call.Temperature != nil {
 292			params.Temperature = param.Opt[float64]{}
 293			warnings = append(warnings, fantasy.CallWarning{
 294				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 295				Setting: "temperature",
 296				Details: "temperature is not supported when thinking is enabled",
 297			})
 298		}
 299		if call.TopP != nil {
 300			params.TopP = param.Opt[float64]{}
 301			warnings = append(warnings, fantasy.CallWarning{
 302				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 303				Setting: "TopP",
 304				Details: "TopP is not supported when thinking is enabled",
 305			})
 306		}
 307		if call.TopK != nil {
 308			params.TopK = param.Opt[int64]{}
 309			warnings = append(warnings, fantasy.CallWarning{
 310				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 311				Setting: "TopK",
 312				Details: "TopK is not supported when thinking is enabled",
 313			})
 314		}
 315	}
 316
 317	if len(call.Tools) > 0 {
 318		disableParallelToolUse := false
 319		if providerOptions.DisableParallelToolUse != nil {
 320			disableParallelToolUse = *providerOptions.DisableParallelToolUse
 321		}
 322		tools, toolChoice, toolWarnings := a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse)
 323		params.Tools = tools
 324		if toolChoice != nil {
 325			params.ToolChoice = *toolChoice
 326		}
 327		warnings = append(warnings, toolWarnings...)
 328	}
 329
 330	return params, warnings, nil
 331}
 332
 333func (a *provider) Name() string {
 334	return Name
 335}
 336
 337// GetCacheControl extracts cache control settings from provider options.
 338func GetCacheControl(providerOptions fantasy.ProviderOptions) *CacheControl {
 339	if anthropicOptions, ok := providerOptions[Name]; ok {
 340		if options, ok := anthropicOptions.(*ProviderCacheControlOptions); ok {
 341			return &options.CacheControl
 342		}
 343	}
 344	return nil
 345}
 346
 347// GetReasoningMetadata extracts reasoning metadata from provider options.
 348func GetReasoningMetadata(providerOptions fantasy.ProviderOptions) *ReasoningOptionMetadata {
 349	if anthropicOptions, ok := providerOptions[Name]; ok {
 350		if reasoning, ok := anthropicOptions.(*ReasoningOptionMetadata); ok {
 351			return reasoning
 352		}
 353	}
 354	return nil
 355}
 356
 357type messageBlock struct {
 358	Role     fantasy.MessageRole
 359	Messages []fantasy.Message
 360}
 361
 362func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock {
 363	var blocks []*messageBlock
 364
 365	var currentBlock *messageBlock
 366
 367	for _, msg := range prompt {
 368		switch msg.Role {
 369		case fantasy.MessageRoleSystem:
 370			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleSystem {
 371				currentBlock = &messageBlock{
 372					Role:     fantasy.MessageRoleSystem,
 373					Messages: []fantasy.Message{},
 374				}
 375				blocks = append(blocks, currentBlock)
 376			}
 377			currentBlock.Messages = append(currentBlock.Messages, msg)
 378		case fantasy.MessageRoleUser:
 379			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
 380				currentBlock = &messageBlock{
 381					Role:     fantasy.MessageRoleUser,
 382					Messages: []fantasy.Message{},
 383				}
 384				blocks = append(blocks, currentBlock)
 385			}
 386			currentBlock.Messages = append(currentBlock.Messages, msg)
 387		case fantasy.MessageRoleAssistant:
 388			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleAssistant {
 389				currentBlock = &messageBlock{
 390					Role:     fantasy.MessageRoleAssistant,
 391					Messages: []fantasy.Message{},
 392				}
 393				blocks = append(blocks, currentBlock)
 394			}
 395			currentBlock.Messages = append(currentBlock.Messages, msg)
 396		case fantasy.MessageRoleTool:
 397			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
 398				currentBlock = &messageBlock{
 399					Role:     fantasy.MessageRoleUser,
 400					Messages: []fantasy.Message{},
 401				}
 402				blocks = append(blocks, currentBlock)
 403			}
 404			currentBlock.Messages = append(currentBlock.Messages, msg)
 405		}
 406	}
 407	return blocks
 408}
 409
 410func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (anthropicTools []anthropic.ToolUnionParam, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning) {
 411	for _, tool := range tools {
 412		if tool.GetType() == fantasy.ToolTypeFunction {
 413			ft, ok := tool.(fantasy.FunctionTool)
 414			if !ok {
 415				continue
 416			}
 417			required := []string{}
 418			var properties any
 419			if props, ok := ft.InputSchema["properties"]; ok {
 420				properties = props
 421			}
 422			if req, ok := ft.InputSchema["required"]; ok {
 423				if reqArr, ok := req.([]string); ok {
 424					required = reqArr
 425				}
 426			}
 427			cacheControl := GetCacheControl(ft.ProviderOptions)
 428
 429			anthropicTool := anthropic.ToolParam{
 430				Name:        ft.Name,
 431				Description: anthropic.String(ft.Description),
 432				InputSchema: anthropic.ToolInputSchemaParam{
 433					Properties: properties,
 434					Required:   required,
 435				},
 436			}
 437			if cacheControl != nil {
 438				anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
 439			}
 440			anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{OfTool: &anthropicTool})
 441			continue
 442		}
 443		// TODO: handle provider tool calls
 444		warnings = append(warnings, fantasy.CallWarning{
 445			Type:    fantasy.CallWarningTypeUnsupportedTool,
 446			Tool:    tool,
 447			Message: "tool is not supported",
 448		})
 449	}
 450
 451	// NOTE: Bedrock does not support this attribute.
 452	var disableParallelToolUse param.Opt[bool]
 453	if !a.options.useBedrock {
 454		disableParallelToolUse = param.NewOpt(disableParallelToolCalls)
 455	}
 456
 457	if toolChoice == nil {
 458		if disableParallelToolCalls {
 459			anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 460				OfAuto: &anthropic.ToolChoiceAutoParam{
 461					Type:                   "auto",
 462					DisableParallelToolUse: disableParallelToolUse,
 463				},
 464			}
 465		}
 466		return anthropicTools, anthropicToolChoice, warnings
 467	}
 468
 469	switch *toolChoice {
 470	case fantasy.ToolChoiceAuto:
 471		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 472			OfAuto: &anthropic.ToolChoiceAutoParam{
 473				Type:                   "auto",
 474				DisableParallelToolUse: disableParallelToolUse,
 475			},
 476		}
 477	case fantasy.ToolChoiceRequired:
 478		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 479			OfAny: &anthropic.ToolChoiceAnyParam{
 480				Type:                   "any",
 481				DisableParallelToolUse: disableParallelToolUse,
 482			},
 483		}
 484	case fantasy.ToolChoiceNone:
 485		return anthropicTools, anthropicToolChoice, warnings
 486	default:
 487		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 488			OfTool: &anthropic.ToolChoiceToolParam{
 489				Type:                   "tool",
 490				Name:                   string(*toolChoice),
 491				DisableParallelToolUse: disableParallelToolUse,
 492			},
 493		}
 494	}
 495	return anthropicTools, anthropicToolChoice, warnings
 496}
 497
 498func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) {
 499	var systemBlocks []anthropic.TextBlockParam
 500	var messages []anthropic.MessageParam
 501	var warnings []fantasy.CallWarning
 502
 503	blocks := groupIntoBlocks(prompt)
 504	finishedSystemBlock := false
 505	for _, block := range blocks {
 506		switch block.Role {
 507		case fantasy.MessageRoleSystem:
 508			if finishedSystemBlock {
 509				// skip multiple system messages that are separated by user/assistant messages
 510				// TODO: see if we need to send error here?
 511				continue
 512			}
 513			finishedSystemBlock = true
 514			for _, msg := range block.Messages {
 515				for i, part := range msg.Content {
 516					isLastPart := i == len(msg.Content)-1
 517					cacheControl := GetCacheControl(part.Options())
 518					if cacheControl == nil && isLastPart {
 519						cacheControl = GetCacheControl(msg.ProviderOptions)
 520					}
 521					text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 522					if !ok {
 523						continue
 524					}
 525					textBlock := anthropic.TextBlockParam{
 526						Text: text.Text,
 527					}
 528					if cacheControl != nil {
 529						textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 530					}
 531					systemBlocks = append(systemBlocks, textBlock)
 532				}
 533			}
 534
 535		case fantasy.MessageRoleUser:
 536			var anthropicContent []anthropic.ContentBlockParamUnion
 537			for _, msg := range block.Messages {
 538				if msg.Role == fantasy.MessageRoleUser {
 539					for i, part := range msg.Content {
 540						isLastPart := i == len(msg.Content)-1
 541						cacheControl := GetCacheControl(part.Options())
 542						if cacheControl == nil && isLastPart {
 543							cacheControl = GetCacheControl(msg.ProviderOptions)
 544						}
 545						switch part.GetType() {
 546						case fantasy.ContentTypeText:
 547							text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 548							if !ok {
 549								continue
 550							}
 551							textBlock := &anthropic.TextBlockParam{
 552								Text: text.Text,
 553							}
 554							if cacheControl != nil {
 555								textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 556							}
 557							anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 558								OfText: textBlock,
 559							})
 560						case fantasy.ContentTypeFile:
 561							file, ok := fantasy.AsMessagePart[fantasy.FilePart](part)
 562							if !ok {
 563								continue
 564							}
 565							// TODO: handle other file types
 566							if !strings.HasPrefix(file.MediaType, "image/") {
 567								continue
 568							}
 569
 570							base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
 571							imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
 572							if cacheControl != nil {
 573								imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
 574							}
 575							anthropicContent = append(anthropicContent, imageBlock)
 576						}
 577					}
 578				} else if msg.Role == fantasy.MessageRoleTool {
 579					for i, part := range msg.Content {
 580						isLastPart := i == len(msg.Content)-1
 581						cacheControl := GetCacheControl(part.Options())
 582						if cacheControl == nil && isLastPart {
 583							cacheControl = GetCacheControl(msg.ProviderOptions)
 584						}
 585						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
 586						if !ok {
 587							continue
 588						}
 589						toolResultBlock := anthropic.ToolResultBlockParam{
 590							ToolUseID: result.ToolCallID,
 591						}
 592						switch result.Output.GetType() {
 593						case fantasy.ToolResultContentTypeText:
 594							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
 595							if !ok {
 596								continue
 597							}
 598							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 599								{
 600									OfText: &anthropic.TextBlockParam{
 601										Text: content.Text,
 602									},
 603								},
 604							}
 605						case fantasy.ToolResultContentTypeMedia:
 606							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
 607							if !ok {
 608								continue
 609							}
 610							contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
 611								{
 612									OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
 613								},
 614							}
 615							if content.Text != "" {
 616								contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
 617									OfText: &anthropic.TextBlockParam{
 618										Text: content.Text,
 619									},
 620								})
 621							}
 622							toolResultBlock.Content = contentBlocks
 623						case fantasy.ToolResultContentTypeError:
 624							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
 625							if !ok {
 626								continue
 627							}
 628							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 629								{
 630									OfText: &anthropic.TextBlockParam{
 631										Text: content.Error.Error(),
 632									},
 633								},
 634							}
 635							toolResultBlock.IsError = param.NewOpt(true)
 636						}
 637						if cacheControl != nil {
 638							toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 639						}
 640						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 641							OfToolResult: &toolResultBlock,
 642						})
 643					}
 644				}
 645			}
 646			if !hasVisibleUserContent(anthropicContent) {
 647				warnings = append(warnings, fantasy.CallWarning{
 648					Type:    fantasy.CallWarningTypeOther,
 649					Message: "dropping empty user message (contains neither user-facing content nor tool results)",
 650				})
 651				continue
 652			}
 653			messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
 654		case fantasy.MessageRoleAssistant:
 655			var anthropicContent []anthropic.ContentBlockParamUnion
 656			for _, msg := range block.Messages {
 657				for i, part := range msg.Content {
 658					isLastPart := i == len(msg.Content)-1
 659					cacheControl := GetCacheControl(part.Options())
 660					if cacheControl == nil && isLastPart {
 661						cacheControl = GetCacheControl(msg.ProviderOptions)
 662					}
 663					switch part.GetType() {
 664					case fantasy.ContentTypeText:
 665						text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 666						if !ok {
 667							continue
 668						}
 669						textBlock := &anthropic.TextBlockParam{
 670							Text: text.Text,
 671						}
 672						if cacheControl != nil {
 673							textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 674						}
 675						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 676							OfText: textBlock,
 677						})
 678					case fantasy.ContentTypeReasoning:
 679						reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
 680						if !ok {
 681							continue
 682						}
 683						if !sendReasoningData {
 684							warnings = append(warnings, fantasy.CallWarning{
 685								Type:    fantasy.CallWarningTypeOther,
 686								Message: "sending reasoning content is disabled for this model",
 687							})
 688							continue
 689						}
 690						reasoningMetadata := GetReasoningMetadata(part.Options())
 691						if reasoningMetadata == nil {
 692							warnings = append(warnings, fantasy.CallWarning{
 693								Type:    fantasy.CallWarningTypeOther,
 694								Message: "unsupported reasoning metadata",
 695							})
 696							continue
 697						}
 698
 699						if reasoningMetadata.Signature != "" {
 700							anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
 701						} else if reasoningMetadata.RedactedData != "" {
 702							anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
 703						} else {
 704							warnings = append(warnings, fantasy.CallWarning{
 705								Type:    fantasy.CallWarningTypeOther,
 706								Message: "unsupported reasoning metadata",
 707							})
 708							continue
 709						}
 710					case fantasy.ContentTypeToolCall:
 711						toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
 712						if !ok {
 713							continue
 714						}
 715						if toolCall.ProviderExecuted {
 716							// TODO: implement provider executed call
 717							continue
 718						}
 719
 720						var inputMap map[string]any
 721						err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
 722						if err != nil {
 723							continue
 724						}
 725						toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
 726						if cacheControl != nil {
 727							toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
 728						}
 729						anthropicContent = append(anthropicContent, toolUseBlock)
 730					case fantasy.ContentTypeToolResult:
 731						// TODO: implement provider executed tool result
 732					}
 733				}
 734			}
 735
 736			if !hasVisibleAssistantContent(anthropicContent) {
 737				warnings = append(warnings, fantasy.CallWarning{
 738					Type:    fantasy.CallWarningTypeOther,
 739					Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
 740				})
 741				continue
 742			}
 743			messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
 744		}
 745	}
 746	return systemBlocks, messages, warnings
 747}
 748
 749func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
 750	for _, block := range content {
 751		if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
 752			return true
 753		}
 754	}
 755	return false
 756}
 757
 758func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
 759	for _, block := range content {
 760		if block.OfText != nil || block.OfToolUse != nil {
 761			return true
 762		}
 763	}
 764	return false
 765}
 766
 767func mapFinishReason(finishReason string) fantasy.FinishReason {
 768	switch finishReason {
 769	case "end_turn", "pause_turn", "stop_sequence":
 770		return fantasy.FinishReasonStop
 771	case "max_tokens":
 772		return fantasy.FinishReasonLength
 773	case "tool_use":
 774		return fantasy.FinishReasonToolCalls
 775	default:
 776		return fantasy.FinishReasonUnknown
 777	}
 778}
 779
 780// Generate implements fantasy.LanguageModel.
 781func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
 782	params, warnings, err := a.prepareParams(call)
 783	if err != nil {
 784		return nil, err
 785	}
 786	response, err := a.client.Messages.New(ctx, *params, callUARequestOptions(call)...)
 787	if err != nil {
 788		return nil, toProviderErr(err)
 789	}
 790
 791	var content []fantasy.Content
 792	for _, block := range response.Content {
 793		switch block.Type {
 794		case "text":
 795			text, ok := block.AsAny().(anthropic.TextBlock)
 796			if !ok {
 797				continue
 798			}
 799			content = append(content, fantasy.TextContent{
 800				Text: text.Text,
 801			})
 802		case "thinking":
 803			reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
 804			if !ok {
 805				continue
 806			}
 807			content = append(content, fantasy.ReasoningContent{
 808				Text: reasoning.Thinking,
 809				ProviderMetadata: fantasy.ProviderMetadata{
 810					Name: &ReasoningOptionMetadata{
 811						Signature: reasoning.Signature,
 812					},
 813				},
 814			})
 815		case "redacted_thinking":
 816			reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
 817			if !ok {
 818				continue
 819			}
 820			content = append(content, fantasy.ReasoningContent{
 821				Text: "",
 822				ProviderMetadata: fantasy.ProviderMetadata{
 823					Name: &ReasoningOptionMetadata{
 824						RedactedData: reasoning.Data,
 825					},
 826				},
 827			})
 828		case "tool_use":
 829			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
 830			if !ok {
 831				continue
 832			}
 833			content = append(content, fantasy.ToolCallContent{
 834				ToolCallID:       toolUse.ID,
 835				ToolName:         toolUse.Name,
 836				Input:            string(toolUse.Input),
 837				ProviderExecuted: false,
 838			})
 839		}
 840	}
 841
 842	return &fantasy.Response{
 843		Content: content,
 844		Usage: fantasy.Usage{
 845			InputTokens:         response.Usage.InputTokens,
 846			OutputTokens:        response.Usage.OutputTokens,
 847			TotalTokens:         response.Usage.InputTokens + response.Usage.OutputTokens,
 848			CacheCreationTokens: response.Usage.CacheCreationInputTokens,
 849			CacheReadTokens:     response.Usage.CacheReadInputTokens,
 850		},
 851		FinishReason:     mapFinishReason(string(response.StopReason)),
 852		ProviderMetadata: fantasy.ProviderMetadata{},
 853		Warnings:         warnings,
 854	}, nil
 855}
 856
 857// Stream implements fantasy.LanguageModel.
 858func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
 859	params, warnings, err := a.prepareParams(call)
 860	if err != nil {
 861		return nil, err
 862	}
 863
 864	stream := a.client.Messages.NewStreaming(ctx, *params, callUARequestOptions(call)...)
 865	acc := anthropic.Message{}
 866	return func(yield func(fantasy.StreamPart) bool) {
 867		if len(warnings) > 0 {
 868			if !yield(fantasy.StreamPart{
 869				Type:     fantasy.StreamPartTypeWarnings,
 870				Warnings: warnings,
 871			}) {
 872				return
 873			}
 874		}
 875
 876		for stream.Next() {
 877			chunk := stream.Current()
 878			_ = acc.Accumulate(chunk)
 879			switch chunk.Type {
 880			case "content_block_start":
 881				contentBlockType := chunk.ContentBlock.Type
 882				switch contentBlockType {
 883				case "text":
 884					if !yield(fantasy.StreamPart{
 885						Type: fantasy.StreamPartTypeTextStart,
 886						ID:   fmt.Sprintf("%d", chunk.Index),
 887					}) {
 888						return
 889					}
 890				case "thinking":
 891					if !yield(fantasy.StreamPart{
 892						Type: fantasy.StreamPartTypeReasoningStart,
 893						ID:   fmt.Sprintf("%d", chunk.Index),
 894					}) {
 895						return
 896					}
 897				case "redacted_thinking":
 898					if !yield(fantasy.StreamPart{
 899						Type: fantasy.StreamPartTypeReasoningStart,
 900						ID:   fmt.Sprintf("%d", chunk.Index),
 901						ProviderMetadata: fantasy.ProviderMetadata{
 902							Name: &ReasoningOptionMetadata{
 903								RedactedData: chunk.ContentBlock.Data,
 904							},
 905						},
 906					}) {
 907						return
 908					}
 909				case "tool_use":
 910					if !yield(fantasy.StreamPart{
 911						Type:          fantasy.StreamPartTypeToolInputStart,
 912						ID:            chunk.ContentBlock.ID,
 913						ToolCallName:  chunk.ContentBlock.Name,
 914						ToolCallInput: "",
 915					}) {
 916						return
 917					}
 918				}
 919			case "content_block_stop":
 920				if len(acc.Content)-1 < int(chunk.Index) {
 921					continue
 922				}
 923				contentBlock := acc.Content[int(chunk.Index)]
 924				switch contentBlock.Type {
 925				case "text":
 926					if !yield(fantasy.StreamPart{
 927						Type: fantasy.StreamPartTypeTextEnd,
 928						ID:   fmt.Sprintf("%d", chunk.Index),
 929					}) {
 930						return
 931					}
 932				case "thinking":
 933					if !yield(fantasy.StreamPart{
 934						Type: fantasy.StreamPartTypeReasoningEnd,
 935						ID:   fmt.Sprintf("%d", chunk.Index),
 936					}) {
 937						return
 938					}
 939				case "tool_use":
 940					if !yield(fantasy.StreamPart{
 941						Type: fantasy.StreamPartTypeToolInputEnd,
 942						ID:   contentBlock.ID,
 943					}) {
 944						return
 945					}
 946					if !yield(fantasy.StreamPart{
 947						Type:          fantasy.StreamPartTypeToolCall,
 948						ID:            contentBlock.ID,
 949						ToolCallName:  contentBlock.Name,
 950						ToolCallInput: string(contentBlock.Input),
 951					}) {
 952						return
 953					}
 954				}
 955			case "content_block_delta":
 956				switch chunk.Delta.Type {
 957				case "text_delta":
 958					if !yield(fantasy.StreamPart{
 959						Type:  fantasy.StreamPartTypeTextDelta,
 960						ID:    fmt.Sprintf("%d", chunk.Index),
 961						Delta: chunk.Delta.Text,
 962					}) {
 963						return
 964					}
 965				case "thinking_delta":
 966					if !yield(fantasy.StreamPart{
 967						Type:  fantasy.StreamPartTypeReasoningDelta,
 968						ID:    fmt.Sprintf("%d", chunk.Index),
 969						Delta: chunk.Delta.Thinking,
 970					}) {
 971						return
 972					}
 973				case "signature_delta":
 974					if !yield(fantasy.StreamPart{
 975						Type: fantasy.StreamPartTypeReasoningDelta,
 976						ID:   fmt.Sprintf("%d", chunk.Index),
 977						ProviderMetadata: fantasy.ProviderMetadata{
 978							Name: &ReasoningOptionMetadata{
 979								Signature: chunk.Delta.Signature,
 980							},
 981						},
 982					}) {
 983						return
 984					}
 985				case "input_json_delta":
 986					if len(acc.Content)-1 < int(chunk.Index) {
 987						continue
 988					}
 989					contentBlock := acc.Content[int(chunk.Index)]
 990					if !yield(fantasy.StreamPart{
 991						Type:          fantasy.StreamPartTypeToolInputDelta,
 992						ID:            contentBlock.ID,
 993						ToolCallInput: chunk.Delta.PartialJSON,
 994					}) {
 995						return
 996					}
 997				}
 998			case "message_stop":
 999			}
1000		}
1001
1002		err := stream.Err()
1003		if err == nil || errors.Is(err, io.EOF) {
1004			yield(fantasy.StreamPart{
1005				Type:         fantasy.StreamPartTypeFinish,
1006				ID:           acc.ID,
1007				FinishReason: mapFinishReason(string(acc.StopReason)),
1008				Usage: fantasy.Usage{
1009					InputTokens:         acc.Usage.InputTokens,
1010					OutputTokens:        acc.Usage.OutputTokens,
1011					TotalTokens:         acc.Usage.InputTokens + acc.Usage.OutputTokens,
1012					CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
1013					CacheReadTokens:     acc.Usage.CacheReadInputTokens,
1014				},
1015				ProviderMetadata: fantasy.ProviderMetadata{},
1016			})
1017			return
1018		} else { //nolint: revive
1019			yield(fantasy.StreamPart{
1020				Type:  fantasy.StreamPartTypeError,
1021				Error: toProviderErr(err),
1022			})
1023			return
1024		}
1025	}, nil
1026}
1027
1028// GenerateObject implements fantasy.LanguageModel.
1029func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1030	switch a.options.objectMode {
1031	case fantasy.ObjectModeText:
1032		return object.GenerateWithText(ctx, a, call)
1033	default:
1034		return object.GenerateWithTool(ctx, a, call)
1035	}
1036}
1037
1038// StreamObject implements fantasy.LanguageModel.
1039func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1040	switch a.options.objectMode {
1041	case fantasy.ObjectModeText:
1042		return object.StreamWithText(ctx, a, call)
1043	default:
1044		return object.StreamWithTool(ctx, a, call)
1045	}
1046}