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