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