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.Error{Title: "invalid argument", Message: "anthropic provider options should be *anthropic.ProviderOptions"}
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.Error{Title: "no budget", Message: "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 mapFinishReason(finishReason string) fantasy.FinishReason {
694 switch finishReason {
695 case "end_turn", "pause_turn", "stop_sequence":
696 return fantasy.FinishReasonStop
697 case "max_tokens":
698 return fantasy.FinishReasonLength
699 case "tool_use":
700 return fantasy.FinishReasonToolCalls
701 default:
702 return fantasy.FinishReasonUnknown
703 }
704}
705
706// Generate implements fantasy.LanguageModel.
707func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
708 params, warnings, err := a.prepareParams(call)
709 if err != nil {
710 return nil, err
711 }
712 response, err := a.client.Messages.New(ctx, *params)
713 if err != nil {
714 return nil, toProviderErr(err)
715 }
716
717 var content []fantasy.Content
718 for _, block := range response.Content {
719 switch block.Type {
720 case "text":
721 text, ok := block.AsAny().(anthropic.TextBlock)
722 if !ok {
723 continue
724 }
725 content = append(content, fantasy.TextContent{
726 Text: text.Text,
727 })
728 case "thinking":
729 reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
730 if !ok {
731 continue
732 }
733 content = append(content, fantasy.ReasoningContent{
734 Text: reasoning.Thinking,
735 ProviderMetadata: fantasy.ProviderMetadata{
736 Name: &ReasoningOptionMetadata{
737 Signature: reasoning.Signature,
738 },
739 },
740 })
741 case "redacted_thinking":
742 reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
743 if !ok {
744 continue
745 }
746 content = append(content, fantasy.ReasoningContent{
747 Text: "",
748 ProviderMetadata: fantasy.ProviderMetadata{
749 Name: &ReasoningOptionMetadata{
750 RedactedData: reasoning.Data,
751 },
752 },
753 })
754 case "tool_use":
755 toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
756 if !ok {
757 continue
758 }
759 content = append(content, fantasy.ToolCallContent{
760 ToolCallID: toolUse.ID,
761 ToolName: toolUse.Name,
762 Input: string(toolUse.Input),
763 ProviderExecuted: false,
764 })
765 }
766 }
767
768 return &fantasy.Response{
769 Content: content,
770 Usage: fantasy.Usage{
771 InputTokens: response.Usage.InputTokens,
772 OutputTokens: response.Usage.OutputTokens,
773 TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
774 CacheCreationTokens: response.Usage.CacheCreationInputTokens,
775 CacheReadTokens: response.Usage.CacheReadInputTokens,
776 },
777 FinishReason: mapFinishReason(string(response.StopReason)),
778 ProviderMetadata: fantasy.ProviderMetadata{},
779 Warnings: warnings,
780 }, nil
781}
782
783// Stream implements fantasy.LanguageModel.
784func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
785 params, warnings, err := a.prepareParams(call)
786 if err != nil {
787 return nil, err
788 }
789
790 stream := a.client.Messages.NewStreaming(ctx, *params)
791 acc := anthropic.Message{}
792 return func(yield func(fantasy.StreamPart) bool) {
793 if len(warnings) > 0 {
794 if !yield(fantasy.StreamPart{
795 Type: fantasy.StreamPartTypeWarnings,
796 Warnings: warnings,
797 }) {
798 return
799 }
800 }
801
802 for stream.Next() {
803 chunk := stream.Current()
804 _ = acc.Accumulate(chunk)
805 switch chunk.Type {
806 case "content_block_start":
807 contentBlockType := chunk.ContentBlock.Type
808 switch contentBlockType {
809 case "text":
810 if !yield(fantasy.StreamPart{
811 Type: fantasy.StreamPartTypeTextStart,
812 ID: fmt.Sprintf("%d", chunk.Index),
813 }) {
814 return
815 }
816 case "thinking":
817 if !yield(fantasy.StreamPart{
818 Type: fantasy.StreamPartTypeReasoningStart,
819 ID: fmt.Sprintf("%d", chunk.Index),
820 }) {
821 return
822 }
823 case "redacted_thinking":
824 if !yield(fantasy.StreamPart{
825 Type: fantasy.StreamPartTypeReasoningStart,
826 ID: fmt.Sprintf("%d", chunk.Index),
827 ProviderMetadata: fantasy.ProviderMetadata{
828 Name: &ReasoningOptionMetadata{
829 RedactedData: chunk.ContentBlock.Data,
830 },
831 },
832 }) {
833 return
834 }
835 case "tool_use":
836 if !yield(fantasy.StreamPart{
837 Type: fantasy.StreamPartTypeToolInputStart,
838 ID: chunk.ContentBlock.ID,
839 ToolCallName: chunk.ContentBlock.Name,
840 ToolCallInput: "",
841 }) {
842 return
843 }
844 }
845 case "content_block_stop":
846 if len(acc.Content)-1 < int(chunk.Index) {
847 continue
848 }
849 contentBlock := acc.Content[int(chunk.Index)]
850 switch contentBlock.Type {
851 case "text":
852 if !yield(fantasy.StreamPart{
853 Type: fantasy.StreamPartTypeTextEnd,
854 ID: fmt.Sprintf("%d", chunk.Index),
855 }) {
856 return
857 }
858 case "thinking":
859 if !yield(fantasy.StreamPart{
860 Type: fantasy.StreamPartTypeReasoningEnd,
861 ID: fmt.Sprintf("%d", chunk.Index),
862 }) {
863 return
864 }
865 case "tool_use":
866 if !yield(fantasy.StreamPart{
867 Type: fantasy.StreamPartTypeToolInputEnd,
868 ID: contentBlock.ID,
869 }) {
870 return
871 }
872 if !yield(fantasy.StreamPart{
873 Type: fantasy.StreamPartTypeToolCall,
874 ID: contentBlock.ID,
875 ToolCallName: contentBlock.Name,
876 ToolCallInput: string(contentBlock.Input),
877 }) {
878 return
879 }
880 }
881 case "content_block_delta":
882 switch chunk.Delta.Type {
883 case "text_delta":
884 if !yield(fantasy.StreamPart{
885 Type: fantasy.StreamPartTypeTextDelta,
886 ID: fmt.Sprintf("%d", chunk.Index),
887 Delta: chunk.Delta.Text,
888 }) {
889 return
890 }
891 case "thinking_delta":
892 if !yield(fantasy.StreamPart{
893 Type: fantasy.StreamPartTypeReasoningDelta,
894 ID: fmt.Sprintf("%d", chunk.Index),
895 Delta: chunk.Delta.Thinking,
896 }) {
897 return
898 }
899 case "signature_delta":
900 if !yield(fantasy.StreamPart{
901 Type: fantasy.StreamPartTypeReasoningDelta,
902 ID: fmt.Sprintf("%d", chunk.Index),
903 ProviderMetadata: fantasy.ProviderMetadata{
904 Name: &ReasoningOptionMetadata{
905 Signature: chunk.Delta.Signature,
906 },
907 },
908 }) {
909 return
910 }
911 case "input_json_delta":
912 if len(acc.Content)-1 < int(chunk.Index) {
913 continue
914 }
915 contentBlock := acc.Content[int(chunk.Index)]
916 if !yield(fantasy.StreamPart{
917 Type: fantasy.StreamPartTypeToolInputDelta,
918 ID: contentBlock.ID,
919 ToolCallInput: chunk.Delta.PartialJSON,
920 }) {
921 return
922 }
923 }
924 case "message_stop":
925 }
926 }
927
928 err := stream.Err()
929 if err == nil || errors.Is(err, io.EOF) {
930 yield(fantasy.StreamPart{
931 Type: fantasy.StreamPartTypeFinish,
932 ID: acc.ID,
933 FinishReason: mapFinishReason(string(acc.StopReason)),
934 Usage: fantasy.Usage{
935 InputTokens: acc.Usage.InputTokens,
936 OutputTokens: acc.Usage.OutputTokens,
937 TotalTokens: acc.Usage.InputTokens + acc.Usage.OutputTokens,
938 CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
939 CacheReadTokens: acc.Usage.CacheReadInputTokens,
940 },
941 ProviderMetadata: fantasy.ProviderMetadata{},
942 })
943 return
944 } else { //nolint: revive
945 yield(fantasy.StreamPart{
946 Type: fantasy.StreamPartTypeError,
947 Error: toProviderErr(err),
948 })
949 return
950 }
951 }, nil
952}