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	}
 302
 303	if len(call.Tools) > 0 {
 304		disableParallelToolUse := false
 305		if providerOptions.DisableParallelToolUse != nil {
 306			disableParallelToolUse = *providerOptions.DisableParallelToolUse
 307		}
 308		tools, toolChoice, toolWarnings := a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse)
 309		params.Tools = tools
 310		if toolChoice != nil {
 311			params.ToolChoice = *toolChoice
 312		}
 313		warnings = append(warnings, toolWarnings...)
 314	}
 315
 316	return params, warnings, nil
 317}
 318
 319func (a *provider) Name() string {
 320	return Name
 321}
 322
 323// GetCacheControl extracts cache control settings from provider options.
 324func GetCacheControl(providerOptions fantasy.ProviderOptions) *CacheControl {
 325	if anthropicOptions, ok := providerOptions[Name]; ok {
 326		if options, ok := anthropicOptions.(*ProviderCacheControlOptions); ok {
 327			return &options.CacheControl
 328		}
 329	}
 330	return nil
 331}
 332
 333// GetReasoningMetadata extracts reasoning metadata from provider options.
 334func GetReasoningMetadata(providerOptions fantasy.ProviderOptions) *ReasoningOptionMetadata {
 335	if anthropicOptions, ok := providerOptions[Name]; ok {
 336		if reasoning, ok := anthropicOptions.(*ReasoningOptionMetadata); ok {
 337			return reasoning
 338		}
 339	}
 340	return nil
 341}
 342
 343type messageBlock struct {
 344	Role     fantasy.MessageRole
 345	Messages []fantasy.Message
 346}
 347
 348func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock {
 349	var blocks []*messageBlock
 350
 351	var currentBlock *messageBlock
 352
 353	for _, msg := range prompt {
 354		switch msg.Role {
 355		case fantasy.MessageRoleSystem:
 356			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleSystem {
 357				currentBlock = &messageBlock{
 358					Role:     fantasy.MessageRoleSystem,
 359					Messages: []fantasy.Message{},
 360				}
 361				blocks = append(blocks, currentBlock)
 362			}
 363			currentBlock.Messages = append(currentBlock.Messages, msg)
 364		case fantasy.MessageRoleUser:
 365			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
 366				currentBlock = &messageBlock{
 367					Role:     fantasy.MessageRoleUser,
 368					Messages: []fantasy.Message{},
 369				}
 370				blocks = append(blocks, currentBlock)
 371			}
 372			currentBlock.Messages = append(currentBlock.Messages, msg)
 373		case fantasy.MessageRoleAssistant:
 374			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleAssistant {
 375				currentBlock = &messageBlock{
 376					Role:     fantasy.MessageRoleAssistant,
 377					Messages: []fantasy.Message{},
 378				}
 379				blocks = append(blocks, currentBlock)
 380			}
 381			currentBlock.Messages = append(currentBlock.Messages, msg)
 382		case fantasy.MessageRoleTool:
 383			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
 384				currentBlock = &messageBlock{
 385					Role:     fantasy.MessageRoleUser,
 386					Messages: []fantasy.Message{},
 387				}
 388				blocks = append(blocks, currentBlock)
 389			}
 390			currentBlock.Messages = append(currentBlock.Messages, msg)
 391		}
 392	}
 393	return blocks
 394}
 395
 396func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (anthropicTools []anthropic.ToolUnionParam, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning) {
 397	for _, tool := range tools {
 398		if tool.GetType() == fantasy.ToolTypeFunction {
 399			ft, ok := tool.(fantasy.FunctionTool)
 400			if !ok {
 401				continue
 402			}
 403			required := []string{}
 404			var properties any
 405			if props, ok := ft.InputSchema["properties"]; ok {
 406				properties = props
 407			}
 408			if req, ok := ft.InputSchema["required"]; ok {
 409				if reqArr, ok := req.([]string); ok {
 410					required = reqArr
 411				}
 412			}
 413			cacheControl := GetCacheControl(ft.ProviderOptions)
 414
 415			anthropicTool := anthropic.ToolParam{
 416				Name:        ft.Name,
 417				Description: anthropic.String(ft.Description),
 418				InputSchema: anthropic.ToolInputSchemaParam{
 419					Properties: properties,
 420					Required:   required,
 421				},
 422			}
 423			if cacheControl != nil {
 424				anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
 425			}
 426			anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{OfTool: &anthropicTool})
 427			continue
 428		}
 429		// TODO: handle provider tool calls
 430		warnings = append(warnings, fantasy.CallWarning{
 431			Type:    fantasy.CallWarningTypeUnsupportedTool,
 432			Tool:    tool,
 433			Message: "tool is not supported",
 434		})
 435	}
 436
 437	// NOTE: Bedrock does not support this attribute.
 438	var disableParallelToolUse param.Opt[bool]
 439	if !a.options.useBedrock {
 440		disableParallelToolUse = param.NewOpt(disableParallelToolCalls)
 441	}
 442
 443	if toolChoice == nil {
 444		if disableParallelToolCalls {
 445			anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 446				OfAuto: &anthropic.ToolChoiceAutoParam{
 447					Type:                   "auto",
 448					DisableParallelToolUse: disableParallelToolUse,
 449				},
 450			}
 451		}
 452		return anthropicTools, anthropicToolChoice, warnings
 453	}
 454
 455	switch *toolChoice {
 456	case fantasy.ToolChoiceAuto:
 457		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 458			OfAuto: &anthropic.ToolChoiceAutoParam{
 459				Type:                   "auto",
 460				DisableParallelToolUse: disableParallelToolUse,
 461			},
 462		}
 463	case fantasy.ToolChoiceRequired:
 464		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 465			OfAny: &anthropic.ToolChoiceAnyParam{
 466				Type:                   "any",
 467				DisableParallelToolUse: disableParallelToolUse,
 468			},
 469		}
 470	case fantasy.ToolChoiceNone:
 471		return anthropicTools, anthropicToolChoice, warnings
 472	default:
 473		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 474			OfTool: &anthropic.ToolChoiceToolParam{
 475				Type:                   "tool",
 476				Name:                   string(*toolChoice),
 477				DisableParallelToolUse: disableParallelToolUse,
 478			},
 479		}
 480	}
 481	return anthropicTools, anthropicToolChoice, warnings
 482}
 483
 484func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) {
 485	var systemBlocks []anthropic.TextBlockParam
 486	var messages []anthropic.MessageParam
 487	var warnings []fantasy.CallWarning
 488
 489	blocks := groupIntoBlocks(prompt)
 490	finishedSystemBlock := false
 491	for _, block := range blocks {
 492		switch block.Role {
 493		case fantasy.MessageRoleSystem:
 494			if finishedSystemBlock {
 495				// skip multiple system messages that are separated by user/assistant messages
 496				// TODO: see if we need to send error here?
 497				continue
 498			}
 499			finishedSystemBlock = true
 500			for _, msg := range block.Messages {
 501				for i, part := range msg.Content {
 502					isLastPart := i == len(msg.Content)-1
 503					cacheControl := GetCacheControl(part.Options())
 504					if cacheControl == nil && isLastPart {
 505						cacheControl = GetCacheControl(msg.ProviderOptions)
 506					}
 507					text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 508					if !ok {
 509						continue
 510					}
 511					textBlock := anthropic.TextBlockParam{
 512						Text: text.Text,
 513					}
 514					if cacheControl != nil {
 515						textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 516					}
 517					systemBlocks = append(systemBlocks, textBlock)
 518				}
 519			}
 520
 521		case fantasy.MessageRoleUser:
 522			var anthropicContent []anthropic.ContentBlockParamUnion
 523			for _, msg := range block.Messages {
 524				if msg.Role == fantasy.MessageRoleUser {
 525					for i, part := range msg.Content {
 526						isLastPart := i == len(msg.Content)-1
 527						cacheControl := GetCacheControl(part.Options())
 528						if cacheControl == nil && isLastPart {
 529							cacheControl = GetCacheControl(msg.ProviderOptions)
 530						}
 531						switch part.GetType() {
 532						case fantasy.ContentTypeText:
 533							text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 534							if !ok {
 535								continue
 536							}
 537							textBlock := &anthropic.TextBlockParam{
 538								Text: text.Text,
 539							}
 540							if cacheControl != nil {
 541								textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 542							}
 543							anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 544								OfText: textBlock,
 545							})
 546						case fantasy.ContentTypeFile:
 547							file, ok := fantasy.AsMessagePart[fantasy.FilePart](part)
 548							if !ok {
 549								continue
 550							}
 551							// TODO: handle other file types
 552							if !strings.HasPrefix(file.MediaType, "image/") {
 553								continue
 554							}
 555
 556							base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
 557							imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
 558							if cacheControl != nil {
 559								imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
 560							}
 561							anthropicContent = append(anthropicContent, imageBlock)
 562						}
 563					}
 564				} else if msg.Role == fantasy.MessageRoleTool {
 565					for i, part := range msg.Content {
 566						isLastPart := i == len(msg.Content)-1
 567						cacheControl := GetCacheControl(part.Options())
 568						if cacheControl == nil && isLastPart {
 569							cacheControl = GetCacheControl(msg.ProviderOptions)
 570						}
 571						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
 572						if !ok {
 573							continue
 574						}
 575						toolResultBlock := anthropic.ToolResultBlockParam{
 576							ToolUseID: result.ToolCallID,
 577						}
 578						switch result.Output.GetType() {
 579						case fantasy.ToolResultContentTypeText:
 580							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
 581							if !ok {
 582								continue
 583							}
 584							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 585								{
 586									OfText: &anthropic.TextBlockParam{
 587										Text: content.Text,
 588									},
 589								},
 590							}
 591						case fantasy.ToolResultContentTypeMedia:
 592							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
 593							if !ok {
 594								continue
 595							}
 596							contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
 597								{
 598									OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
 599								},
 600							}
 601							if content.Text != "" {
 602								contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
 603									OfText: &anthropic.TextBlockParam{
 604										Text: content.Text,
 605									},
 606								})
 607							}
 608							toolResultBlock.Content = contentBlocks
 609						case fantasy.ToolResultContentTypeError:
 610							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
 611							if !ok {
 612								continue
 613							}
 614							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 615								{
 616									OfText: &anthropic.TextBlockParam{
 617										Text: content.Error.Error(),
 618									},
 619								},
 620							}
 621							toolResultBlock.IsError = param.NewOpt(true)
 622						}
 623						if cacheControl != nil {
 624							toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 625						}
 626						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 627							OfToolResult: &toolResultBlock,
 628						})
 629					}
 630				}
 631			}
 632			if !hasVisibleUserContent(anthropicContent) {
 633				warnings = append(warnings, fantasy.CallWarning{
 634					Type:    fantasy.CallWarningTypeOther,
 635					Message: "dropping empty user message (contains neither user-facing content nor tool results)",
 636				})
 637				continue
 638			}
 639			messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
 640		case fantasy.MessageRoleAssistant:
 641			var anthropicContent []anthropic.ContentBlockParamUnion
 642			for _, msg := range block.Messages {
 643				for i, part := range msg.Content {
 644					isLastPart := i == len(msg.Content)-1
 645					cacheControl := GetCacheControl(part.Options())
 646					if cacheControl == nil && isLastPart {
 647						cacheControl = GetCacheControl(msg.ProviderOptions)
 648					}
 649					switch part.GetType() {
 650					case fantasy.ContentTypeText:
 651						text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 652						if !ok {
 653							continue
 654						}
 655						textBlock := &anthropic.TextBlockParam{
 656							Text: text.Text,
 657						}
 658						if cacheControl != nil {
 659							textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 660						}
 661						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 662							OfText: textBlock,
 663						})
 664					case fantasy.ContentTypeReasoning:
 665						reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
 666						if !ok {
 667							continue
 668						}
 669						if !sendReasoningData {
 670							warnings = append(warnings, fantasy.CallWarning{
 671								Type:    fantasy.CallWarningTypeOther,
 672								Message: "sending reasoning content is disabled for this model",
 673							})
 674							continue
 675						}
 676						reasoningMetadata := GetReasoningMetadata(part.Options())
 677						if reasoningMetadata == nil {
 678							warnings = append(warnings, fantasy.CallWarning{
 679								Type:    fantasy.CallWarningTypeOther,
 680								Message: "unsupported reasoning metadata",
 681							})
 682							continue
 683						}
 684
 685						if reasoningMetadata.Signature != "" {
 686							anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
 687						} else if reasoningMetadata.RedactedData != "" {
 688							anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
 689						} else {
 690							warnings = append(warnings, fantasy.CallWarning{
 691								Type:    fantasy.CallWarningTypeOther,
 692								Message: "unsupported reasoning metadata",
 693							})
 694							continue
 695						}
 696					case fantasy.ContentTypeToolCall:
 697						toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
 698						if !ok {
 699							continue
 700						}
 701						if toolCall.ProviderExecuted {
 702							// TODO: implement provider executed call
 703							continue
 704						}
 705
 706						var inputMap map[string]any
 707						err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
 708						if err != nil {
 709							continue
 710						}
 711						toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
 712						if cacheControl != nil {
 713							toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
 714						}
 715						anthropicContent = append(anthropicContent, toolUseBlock)
 716					case fantasy.ContentTypeToolResult:
 717						// TODO: implement provider executed tool result
 718					}
 719				}
 720			}
 721
 722			if !hasVisibleAssistantContent(anthropicContent) {
 723				warnings = append(warnings, fantasy.CallWarning{
 724					Type:    fantasy.CallWarningTypeOther,
 725					Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
 726				})
 727				continue
 728			}
 729			messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
 730		}
 731	}
 732	return systemBlocks, messages, warnings
 733}
 734
 735func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
 736	for _, block := range content {
 737		if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
 738			return true
 739		}
 740	}
 741	return false
 742}
 743
 744func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
 745	for _, block := range content {
 746		if block.OfText != nil || block.OfToolUse != nil {
 747			return true
 748		}
 749	}
 750	return false
 751}
 752
 753func mapFinishReason(finishReason string) fantasy.FinishReason {
 754	switch finishReason {
 755	case "end_turn", "pause_turn", "stop_sequence":
 756		return fantasy.FinishReasonStop
 757	case "max_tokens":
 758		return fantasy.FinishReasonLength
 759	case "tool_use":
 760		return fantasy.FinishReasonToolCalls
 761	default:
 762		return fantasy.FinishReasonUnknown
 763	}
 764}
 765
 766// Generate implements fantasy.LanguageModel.
 767func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
 768	params, warnings, err := a.prepareParams(call)
 769	if err != nil {
 770		return nil, err
 771	}
 772	response, err := a.client.Messages.New(ctx, *params)
 773	if err != nil {
 774		return nil, toProviderErr(err)
 775	}
 776
 777	var content []fantasy.Content
 778	for _, block := range response.Content {
 779		switch block.Type {
 780		case "text":
 781			text, ok := block.AsAny().(anthropic.TextBlock)
 782			if !ok {
 783				continue
 784			}
 785			content = append(content, fantasy.TextContent{
 786				Text: text.Text,
 787			})
 788		case "thinking":
 789			reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
 790			if !ok {
 791				continue
 792			}
 793			content = append(content, fantasy.ReasoningContent{
 794				Text: reasoning.Thinking,
 795				ProviderMetadata: fantasy.ProviderMetadata{
 796					Name: &ReasoningOptionMetadata{
 797						Signature: reasoning.Signature,
 798					},
 799				},
 800			})
 801		case "redacted_thinking":
 802			reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
 803			if !ok {
 804				continue
 805			}
 806			content = append(content, fantasy.ReasoningContent{
 807				Text: "",
 808				ProviderMetadata: fantasy.ProviderMetadata{
 809					Name: &ReasoningOptionMetadata{
 810						RedactedData: reasoning.Data,
 811					},
 812				},
 813			})
 814		case "tool_use":
 815			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
 816			if !ok {
 817				continue
 818			}
 819			content = append(content, fantasy.ToolCallContent{
 820				ToolCallID:       toolUse.ID,
 821				ToolName:         toolUse.Name,
 822				Input:            string(toolUse.Input),
 823				ProviderExecuted: false,
 824			})
 825		}
 826	}
 827
 828	return &fantasy.Response{
 829		Content: content,
 830		Usage: fantasy.Usage{
 831			InputTokens:         response.Usage.InputTokens,
 832			OutputTokens:        response.Usage.OutputTokens,
 833			TotalTokens:         response.Usage.InputTokens + response.Usage.OutputTokens,
 834			CacheCreationTokens: response.Usage.CacheCreationInputTokens,
 835			CacheReadTokens:     response.Usage.CacheReadInputTokens,
 836		},
 837		FinishReason:     mapFinishReason(string(response.StopReason)),
 838		ProviderMetadata: fantasy.ProviderMetadata{},
 839		Warnings:         warnings,
 840	}, nil
 841}
 842
 843// Stream implements fantasy.LanguageModel.
 844func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
 845	params, warnings, err := a.prepareParams(call)
 846	if err != nil {
 847		return nil, err
 848	}
 849
 850	stream := a.client.Messages.NewStreaming(ctx, *params)
 851	acc := anthropic.Message{}
 852	return func(yield func(fantasy.StreamPart) bool) {
 853		if len(warnings) > 0 {
 854			if !yield(fantasy.StreamPart{
 855				Type:     fantasy.StreamPartTypeWarnings,
 856				Warnings: warnings,
 857			}) {
 858				return
 859			}
 860		}
 861
 862		for stream.Next() {
 863			chunk := stream.Current()
 864			_ = acc.Accumulate(chunk)
 865			switch chunk.Type {
 866			case "content_block_start":
 867				contentBlockType := chunk.ContentBlock.Type
 868				switch contentBlockType {
 869				case "text":
 870					if !yield(fantasy.StreamPart{
 871						Type: fantasy.StreamPartTypeTextStart,
 872						ID:   fmt.Sprintf("%d", chunk.Index),
 873					}) {
 874						return
 875					}
 876				case "thinking":
 877					if !yield(fantasy.StreamPart{
 878						Type: fantasy.StreamPartTypeReasoningStart,
 879						ID:   fmt.Sprintf("%d", chunk.Index),
 880					}) {
 881						return
 882					}
 883				case "redacted_thinking":
 884					if !yield(fantasy.StreamPart{
 885						Type: fantasy.StreamPartTypeReasoningStart,
 886						ID:   fmt.Sprintf("%d", chunk.Index),
 887						ProviderMetadata: fantasy.ProviderMetadata{
 888							Name: &ReasoningOptionMetadata{
 889								RedactedData: chunk.ContentBlock.Data,
 890							},
 891						},
 892					}) {
 893						return
 894					}
 895				case "tool_use":
 896					if !yield(fantasy.StreamPart{
 897						Type:          fantasy.StreamPartTypeToolInputStart,
 898						ID:            chunk.ContentBlock.ID,
 899						ToolCallName:  chunk.ContentBlock.Name,
 900						ToolCallInput: "",
 901					}) {
 902						return
 903					}
 904				}
 905			case "content_block_stop":
 906				if len(acc.Content)-1 < int(chunk.Index) {
 907					continue
 908				}
 909				contentBlock := acc.Content[int(chunk.Index)]
 910				switch contentBlock.Type {
 911				case "text":
 912					if !yield(fantasy.StreamPart{
 913						Type: fantasy.StreamPartTypeTextEnd,
 914						ID:   fmt.Sprintf("%d", chunk.Index),
 915					}) {
 916						return
 917					}
 918				case "thinking":
 919					if !yield(fantasy.StreamPart{
 920						Type: fantasy.StreamPartTypeReasoningEnd,
 921						ID:   fmt.Sprintf("%d", chunk.Index),
 922					}) {
 923						return
 924					}
 925				case "tool_use":
 926					if !yield(fantasy.StreamPart{
 927						Type: fantasy.StreamPartTypeToolInputEnd,
 928						ID:   contentBlock.ID,
 929					}) {
 930						return
 931					}
 932					if !yield(fantasy.StreamPart{
 933						Type:          fantasy.StreamPartTypeToolCall,
 934						ID:            contentBlock.ID,
 935						ToolCallName:  contentBlock.Name,
 936						ToolCallInput: string(contentBlock.Input),
 937					}) {
 938						return
 939					}
 940				}
 941			case "content_block_delta":
 942				switch chunk.Delta.Type {
 943				case "text_delta":
 944					if !yield(fantasy.StreamPart{
 945						Type:  fantasy.StreamPartTypeTextDelta,
 946						ID:    fmt.Sprintf("%d", chunk.Index),
 947						Delta: chunk.Delta.Text,
 948					}) {
 949						return
 950					}
 951				case "thinking_delta":
 952					if !yield(fantasy.StreamPart{
 953						Type:  fantasy.StreamPartTypeReasoningDelta,
 954						ID:    fmt.Sprintf("%d", chunk.Index),
 955						Delta: chunk.Delta.Thinking,
 956					}) {
 957						return
 958					}
 959				case "signature_delta":
 960					if !yield(fantasy.StreamPart{
 961						Type: fantasy.StreamPartTypeReasoningDelta,
 962						ID:   fmt.Sprintf("%d", chunk.Index),
 963						ProviderMetadata: fantasy.ProviderMetadata{
 964							Name: &ReasoningOptionMetadata{
 965								Signature: chunk.Delta.Signature,
 966							},
 967						},
 968					}) {
 969						return
 970					}
 971				case "input_json_delta":
 972					if len(acc.Content)-1 < int(chunk.Index) {
 973						continue
 974					}
 975					contentBlock := acc.Content[int(chunk.Index)]
 976					if !yield(fantasy.StreamPart{
 977						Type:          fantasy.StreamPartTypeToolInputDelta,
 978						ID:            contentBlock.ID,
 979						ToolCallInput: chunk.Delta.PartialJSON,
 980					}) {
 981						return
 982					}
 983				}
 984			case "message_stop":
 985			}
 986		}
 987
 988		err := stream.Err()
 989		if err == nil || errors.Is(err, io.EOF) {
 990			yield(fantasy.StreamPart{
 991				Type:         fantasy.StreamPartTypeFinish,
 992				ID:           acc.ID,
 993				FinishReason: mapFinishReason(string(acc.StopReason)),
 994				Usage: fantasy.Usage{
 995					InputTokens:         acc.Usage.InputTokens,
 996					OutputTokens:        acc.Usage.OutputTokens,
 997					TotalTokens:         acc.Usage.InputTokens + acc.Usage.OutputTokens,
 998					CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
 999					CacheReadTokens:     acc.Usage.CacheReadInputTokens,
1000				},
1001				ProviderMetadata: fantasy.ProviderMetadata{},
1002			})
1003			return
1004		} else { //nolint: revive
1005			yield(fantasy.StreamPart{
1006				Type:  fantasy.StreamPartTypeError,
1007				Error: toProviderErr(err),
1008			})
1009			return
1010		}
1011	}, nil
1012}
1013
1014// GenerateObject implements fantasy.LanguageModel.
1015func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1016	switch a.options.objectMode {
1017	case fantasy.ObjectModeText:
1018		return object.GenerateWithText(ctx, a, call)
1019	default:
1020		return object.GenerateWithTool(ctx, a, call)
1021	}
1022}
1023
1024// StreamObject implements fantasy.LanguageModel.
1025func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1026	switch a.options.objectMode {
1027	case fantasy.ObjectModeText:
1028		return object.StreamWithText(ctx, a, call)
1029	default:
1030		return object.StreamWithTool(ctx, a, call)
1031	}
1032}