anthropic.go

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