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