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