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							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 595								{
 596									OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
 597								},
 598							}
 599						case fantasy.ToolResultContentTypeError:
 600							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
 601							if !ok {
 602								continue
 603							}
 604							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
 605								{
 606									OfText: &anthropic.TextBlockParam{
 607										Text: content.Error.Error(),
 608									},
 609								},
 610							}
 611							toolResultBlock.IsError = param.NewOpt(true)
 612						}
 613						if cacheControl != nil {
 614							toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 615						}
 616						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 617							OfToolResult: &toolResultBlock,
 618						})
 619					}
 620				}
 621			}
 622			if !hasVisibleUserContent(anthropicContent) {
 623				warnings = append(warnings, fantasy.CallWarning{
 624					Type:    fantasy.CallWarningTypeOther,
 625					Message: "dropping empty user message (contains neither user-facing content nor tool results)",
 626				})
 627				continue
 628			}
 629			messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
 630		case fantasy.MessageRoleAssistant:
 631			var anthropicContent []anthropic.ContentBlockParamUnion
 632			for _, msg := range block.Messages {
 633				for i, part := range msg.Content {
 634					isLastPart := i == len(msg.Content)-1
 635					cacheControl := GetCacheControl(part.Options())
 636					if cacheControl == nil && isLastPart {
 637						cacheControl = GetCacheControl(msg.ProviderOptions)
 638					}
 639					switch part.GetType() {
 640					case fantasy.ContentTypeText:
 641						text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
 642						if !ok {
 643							continue
 644						}
 645						textBlock := &anthropic.TextBlockParam{
 646							Text: text.Text,
 647						}
 648						if cacheControl != nil {
 649							textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
 650						}
 651						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
 652							OfText: textBlock,
 653						})
 654					case fantasy.ContentTypeReasoning:
 655						reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
 656						if !ok {
 657							continue
 658						}
 659						if !sendReasoningData {
 660							warnings = append(warnings, fantasy.CallWarning{
 661								Type:    fantasy.CallWarningTypeOther,
 662								Message: "sending reasoning content is disabled for this model",
 663							})
 664							continue
 665						}
 666						reasoningMetadata := GetReasoningMetadata(part.Options())
 667						if reasoningMetadata == nil {
 668							warnings = append(warnings, fantasy.CallWarning{
 669								Type:    fantasy.CallWarningTypeOther,
 670								Message: "unsupported reasoning metadata",
 671							})
 672							continue
 673						}
 674
 675						if reasoningMetadata.Signature != "" {
 676							anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
 677						} else if reasoningMetadata.RedactedData != "" {
 678							anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
 679						} else {
 680							warnings = append(warnings, fantasy.CallWarning{
 681								Type:    fantasy.CallWarningTypeOther,
 682								Message: "unsupported reasoning metadata",
 683							})
 684							continue
 685						}
 686					case fantasy.ContentTypeToolCall:
 687						toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
 688						if !ok {
 689							continue
 690						}
 691						if toolCall.ProviderExecuted {
 692							// TODO: implement provider executed call
 693							continue
 694						}
 695
 696						var inputMap map[string]any
 697						err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
 698						if err != nil {
 699							continue
 700						}
 701						toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
 702						if cacheControl != nil {
 703							toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
 704						}
 705						anthropicContent = append(anthropicContent, toolUseBlock)
 706					case fantasy.ContentTypeToolResult:
 707						// TODO: implement provider executed tool result
 708					}
 709				}
 710			}
 711
 712			if !hasVisibleAssistantContent(anthropicContent) {
 713				warnings = append(warnings, fantasy.CallWarning{
 714					Type:    fantasy.CallWarningTypeOther,
 715					Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
 716				})
 717				continue
 718			}
 719			messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
 720		}
 721	}
 722	return systemBlocks, messages, warnings
 723}
 724
 725func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
 726	for _, block := range content {
 727		if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
 728			return true
 729		}
 730	}
 731	return false
 732}
 733
 734func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
 735	for _, block := range content {
 736		if block.OfText != nil || block.OfToolUse != nil {
 737			return true
 738		}
 739	}
 740	return false
 741}
 742
 743func mapFinishReason(finishReason string) fantasy.FinishReason {
 744	switch finishReason {
 745	case "end_turn", "pause_turn", "stop_sequence":
 746		return fantasy.FinishReasonStop
 747	case "max_tokens":
 748		return fantasy.FinishReasonLength
 749	case "tool_use":
 750		return fantasy.FinishReasonToolCalls
 751	default:
 752		return fantasy.FinishReasonUnknown
 753	}
 754}
 755
 756// Generate implements fantasy.LanguageModel.
 757func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
 758	params, warnings, err := a.prepareParams(call)
 759	if err != nil {
 760		return nil, err
 761	}
 762	response, err := a.client.Messages.New(ctx, *params)
 763	if err != nil {
 764		return nil, toProviderErr(err)
 765	}
 766
 767	var content []fantasy.Content
 768	for _, block := range response.Content {
 769		switch block.Type {
 770		case "text":
 771			text, ok := block.AsAny().(anthropic.TextBlock)
 772			if !ok {
 773				continue
 774			}
 775			content = append(content, fantasy.TextContent{
 776				Text: text.Text,
 777			})
 778		case "thinking":
 779			reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
 780			if !ok {
 781				continue
 782			}
 783			content = append(content, fantasy.ReasoningContent{
 784				Text: reasoning.Thinking,
 785				ProviderMetadata: fantasy.ProviderMetadata{
 786					Name: &ReasoningOptionMetadata{
 787						Signature: reasoning.Signature,
 788					},
 789				},
 790			})
 791		case "redacted_thinking":
 792			reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
 793			if !ok {
 794				continue
 795			}
 796			content = append(content, fantasy.ReasoningContent{
 797				Text: "",
 798				ProviderMetadata: fantasy.ProviderMetadata{
 799					Name: &ReasoningOptionMetadata{
 800						RedactedData: reasoning.Data,
 801					},
 802				},
 803			})
 804		case "tool_use":
 805			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
 806			if !ok {
 807				continue
 808			}
 809			content = append(content, fantasy.ToolCallContent{
 810				ToolCallID:       toolUse.ID,
 811				ToolName:         toolUse.Name,
 812				Input:            string(toolUse.Input),
 813				ProviderExecuted: false,
 814			})
 815		}
 816	}
 817
 818	return &fantasy.Response{
 819		Content: content,
 820		Usage: fantasy.Usage{
 821			InputTokens:         response.Usage.InputTokens,
 822			OutputTokens:        response.Usage.OutputTokens,
 823			TotalTokens:         response.Usage.InputTokens + response.Usage.OutputTokens,
 824			CacheCreationTokens: response.Usage.CacheCreationInputTokens,
 825			CacheReadTokens:     response.Usage.CacheReadInputTokens,
 826		},
 827		FinishReason:     mapFinishReason(string(response.StopReason)),
 828		ProviderMetadata: fantasy.ProviderMetadata{},
 829		Warnings:         warnings,
 830	}, nil
 831}
 832
 833// Stream implements fantasy.LanguageModel.
 834func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
 835	params, warnings, err := a.prepareParams(call)
 836	if err != nil {
 837		return nil, err
 838	}
 839
 840	stream := a.client.Messages.NewStreaming(ctx, *params)
 841	acc := anthropic.Message{}
 842	return func(yield func(fantasy.StreamPart) bool) {
 843		if len(warnings) > 0 {
 844			if !yield(fantasy.StreamPart{
 845				Type:     fantasy.StreamPartTypeWarnings,
 846				Warnings: warnings,
 847			}) {
 848				return
 849			}
 850		}
 851
 852		for stream.Next() {
 853			chunk := stream.Current()
 854			_ = acc.Accumulate(chunk)
 855			switch chunk.Type {
 856			case "content_block_start":
 857				contentBlockType := chunk.ContentBlock.Type
 858				switch contentBlockType {
 859				case "text":
 860					if !yield(fantasy.StreamPart{
 861						Type: fantasy.StreamPartTypeTextStart,
 862						ID:   fmt.Sprintf("%d", chunk.Index),
 863					}) {
 864						return
 865					}
 866				case "thinking":
 867					if !yield(fantasy.StreamPart{
 868						Type: fantasy.StreamPartTypeReasoningStart,
 869						ID:   fmt.Sprintf("%d", chunk.Index),
 870					}) {
 871						return
 872					}
 873				case "redacted_thinking":
 874					if !yield(fantasy.StreamPart{
 875						Type: fantasy.StreamPartTypeReasoningStart,
 876						ID:   fmt.Sprintf("%d", chunk.Index),
 877						ProviderMetadata: fantasy.ProviderMetadata{
 878							Name: &ReasoningOptionMetadata{
 879								RedactedData: chunk.ContentBlock.Data,
 880							},
 881						},
 882					}) {
 883						return
 884					}
 885				case "tool_use":
 886					if !yield(fantasy.StreamPart{
 887						Type:          fantasy.StreamPartTypeToolInputStart,
 888						ID:            chunk.ContentBlock.ID,
 889						ToolCallName:  chunk.ContentBlock.Name,
 890						ToolCallInput: "",
 891					}) {
 892						return
 893					}
 894				}
 895			case "content_block_stop":
 896				if len(acc.Content)-1 < int(chunk.Index) {
 897					continue
 898				}
 899				contentBlock := acc.Content[int(chunk.Index)]
 900				switch contentBlock.Type {
 901				case "text":
 902					if !yield(fantasy.StreamPart{
 903						Type: fantasy.StreamPartTypeTextEnd,
 904						ID:   fmt.Sprintf("%d", chunk.Index),
 905					}) {
 906						return
 907					}
 908				case "thinking":
 909					if !yield(fantasy.StreamPart{
 910						Type: fantasy.StreamPartTypeReasoningEnd,
 911						ID:   fmt.Sprintf("%d", chunk.Index),
 912					}) {
 913						return
 914					}
 915				case "tool_use":
 916					if !yield(fantasy.StreamPart{
 917						Type: fantasy.StreamPartTypeToolInputEnd,
 918						ID:   contentBlock.ID,
 919					}) {
 920						return
 921					}
 922					if !yield(fantasy.StreamPart{
 923						Type:          fantasy.StreamPartTypeToolCall,
 924						ID:            contentBlock.ID,
 925						ToolCallName:  contentBlock.Name,
 926						ToolCallInput: string(contentBlock.Input),
 927					}) {
 928						return
 929					}
 930				}
 931			case "content_block_delta":
 932				switch chunk.Delta.Type {
 933				case "text_delta":
 934					if !yield(fantasy.StreamPart{
 935						Type:  fantasy.StreamPartTypeTextDelta,
 936						ID:    fmt.Sprintf("%d", chunk.Index),
 937						Delta: chunk.Delta.Text,
 938					}) {
 939						return
 940					}
 941				case "thinking_delta":
 942					if !yield(fantasy.StreamPart{
 943						Type:  fantasy.StreamPartTypeReasoningDelta,
 944						ID:    fmt.Sprintf("%d", chunk.Index),
 945						Delta: chunk.Delta.Thinking,
 946					}) {
 947						return
 948					}
 949				case "signature_delta":
 950					if !yield(fantasy.StreamPart{
 951						Type: fantasy.StreamPartTypeReasoningDelta,
 952						ID:   fmt.Sprintf("%d", chunk.Index),
 953						ProviderMetadata: fantasy.ProviderMetadata{
 954							Name: &ReasoningOptionMetadata{
 955								Signature: chunk.Delta.Signature,
 956							},
 957						},
 958					}) {
 959						return
 960					}
 961				case "input_json_delta":
 962					if len(acc.Content)-1 < int(chunk.Index) {
 963						continue
 964					}
 965					contentBlock := acc.Content[int(chunk.Index)]
 966					if !yield(fantasy.StreamPart{
 967						Type:          fantasy.StreamPartTypeToolInputDelta,
 968						ID:            contentBlock.ID,
 969						ToolCallInput: chunk.Delta.PartialJSON,
 970					}) {
 971						return
 972					}
 973				}
 974			case "message_stop":
 975			}
 976		}
 977
 978		err := stream.Err()
 979		if err == nil || errors.Is(err, io.EOF) {
 980			yield(fantasy.StreamPart{
 981				Type:         fantasy.StreamPartTypeFinish,
 982				ID:           acc.ID,
 983				FinishReason: mapFinishReason(string(acc.StopReason)),
 984				Usage: fantasy.Usage{
 985					InputTokens:         acc.Usage.InputTokens,
 986					OutputTokens:        acc.Usage.OutputTokens,
 987					TotalTokens:         acc.Usage.InputTokens + acc.Usage.OutputTokens,
 988					CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
 989					CacheReadTokens:     acc.Usage.CacheReadInputTokens,
 990				},
 991				ProviderMetadata: fantasy.ProviderMetadata{},
 992			})
 993			return
 994		} else { //nolint: revive
 995			yield(fantasy.StreamPart{
 996				Type:  fantasy.StreamPartTypeError,
 997				Error: toProviderErr(err),
 998			})
 999			return
1000		}
1001	}, nil
1002}
1003
1004// GenerateObject implements fantasy.LanguageModel.
1005func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1006	switch a.options.objectMode {
1007	case fantasy.ObjectModeText:
1008		return object.GenerateWithText(ctx, a, call)
1009	default:
1010		return object.GenerateWithTool(ctx, a, call)
1011	}
1012}
1013
1014// StreamObject implements fantasy.LanguageModel.
1015func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1016	switch a.options.objectMode {
1017	case fantasy.ObjectModeText:
1018		return object.StreamWithText(ctx, a, call)
1019	default:
1020		return object.StreamWithTool(ctx, a, call)
1021	}
1022}