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