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