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