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