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