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 contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
595 {
596 OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
597 },
598 }
599 if content.Text != "" {
600 contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
601 OfText: &anthropic.TextBlockParam{
602 Text: content.Text,
603 },
604 })
605 }
606 toolResultBlock.Content = contentBlocks
607 case fantasy.ToolResultContentTypeError:
608 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
609 if !ok {
610 continue
611 }
612 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
613 {
614 OfText: &anthropic.TextBlockParam{
615 Text: content.Error.Error(),
616 },
617 },
618 }
619 toolResultBlock.IsError = param.NewOpt(true)
620 }
621 if cacheControl != nil {
622 toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
623 }
624 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
625 OfToolResult: &toolResultBlock,
626 })
627 }
628 }
629 }
630 if !hasVisibleUserContent(anthropicContent) {
631 warnings = append(warnings, fantasy.CallWarning{
632 Type: fantasy.CallWarningTypeOther,
633 Message: "dropping empty user message (contains neither user-facing content nor tool results)",
634 })
635 continue
636 }
637 messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
638 case fantasy.MessageRoleAssistant:
639 var anthropicContent []anthropic.ContentBlockParamUnion
640 for _, msg := range block.Messages {
641 for i, part := range msg.Content {
642 isLastPart := i == len(msg.Content)-1
643 cacheControl := GetCacheControl(part.Options())
644 if cacheControl == nil && isLastPart {
645 cacheControl = GetCacheControl(msg.ProviderOptions)
646 }
647 switch part.GetType() {
648 case fantasy.ContentTypeText:
649 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
650 if !ok {
651 continue
652 }
653 textBlock := &anthropic.TextBlockParam{
654 Text: text.Text,
655 }
656 if cacheControl != nil {
657 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
658 }
659 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
660 OfText: textBlock,
661 })
662 case fantasy.ContentTypeReasoning:
663 reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
664 if !ok {
665 continue
666 }
667 if !sendReasoningData {
668 warnings = append(warnings, fantasy.CallWarning{
669 Type: fantasy.CallWarningTypeOther,
670 Message: "sending reasoning content is disabled for this model",
671 })
672 continue
673 }
674 reasoningMetadata := GetReasoningMetadata(part.Options())
675 if reasoningMetadata == nil {
676 warnings = append(warnings, fantasy.CallWarning{
677 Type: fantasy.CallWarningTypeOther,
678 Message: "unsupported reasoning metadata",
679 })
680 continue
681 }
682
683 if reasoningMetadata.Signature != "" {
684 anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
685 } else if reasoningMetadata.RedactedData != "" {
686 anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
687 } else {
688 warnings = append(warnings, fantasy.CallWarning{
689 Type: fantasy.CallWarningTypeOther,
690 Message: "unsupported reasoning metadata",
691 })
692 continue
693 }
694 case fantasy.ContentTypeToolCall:
695 toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
696 if !ok {
697 continue
698 }
699 if toolCall.ProviderExecuted {
700 // TODO: implement provider executed call
701 continue
702 }
703
704 var inputMap map[string]any
705 err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
706 if err != nil {
707 continue
708 }
709 toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
710 if cacheControl != nil {
711 toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
712 }
713 anthropicContent = append(anthropicContent, toolUseBlock)
714 case fantasy.ContentTypeToolResult:
715 // TODO: implement provider executed tool result
716 }
717 }
718 }
719
720 if !hasVisibleAssistantContent(anthropicContent) {
721 warnings = append(warnings, fantasy.CallWarning{
722 Type: fantasy.CallWarningTypeOther,
723 Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
724 })
725 continue
726 }
727 messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
728 }
729 }
730 return systemBlocks, messages, warnings
731}
732
733func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
734 for _, block := range content {
735 if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
736 return true
737 }
738 }
739 return false
740}
741
742func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
743 for _, block := range content {
744 if block.OfText != nil || block.OfToolUse != nil {
745 return true
746 }
747 }
748 return false
749}
750
751func mapFinishReason(finishReason string) fantasy.FinishReason {
752 switch finishReason {
753 case "end_turn", "pause_turn", "stop_sequence":
754 return fantasy.FinishReasonStop
755 case "max_tokens":
756 return fantasy.FinishReasonLength
757 case "tool_use":
758 return fantasy.FinishReasonToolCalls
759 default:
760 return fantasy.FinishReasonUnknown
761 }
762}
763
764// Generate implements fantasy.LanguageModel.
765func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
766 params, warnings, err := a.prepareParams(call)
767 if err != nil {
768 return nil, err
769 }
770 response, err := a.client.Messages.New(ctx, *params)
771 if err != nil {
772 return nil, toProviderErr(err)
773 }
774
775 var content []fantasy.Content
776 for _, block := range response.Content {
777 switch block.Type {
778 case "text":
779 text, ok := block.AsAny().(anthropic.TextBlock)
780 if !ok {
781 continue
782 }
783 content = append(content, fantasy.TextContent{
784 Text: text.Text,
785 })
786 case "thinking":
787 reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
788 if !ok {
789 continue
790 }
791 content = append(content, fantasy.ReasoningContent{
792 Text: reasoning.Thinking,
793 ProviderMetadata: fantasy.ProviderMetadata{
794 Name: &ReasoningOptionMetadata{
795 Signature: reasoning.Signature,
796 },
797 },
798 })
799 case "redacted_thinking":
800 reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
801 if !ok {
802 continue
803 }
804 content = append(content, fantasy.ReasoningContent{
805 Text: "",
806 ProviderMetadata: fantasy.ProviderMetadata{
807 Name: &ReasoningOptionMetadata{
808 RedactedData: reasoning.Data,
809 },
810 },
811 })
812 case "tool_use":
813 toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
814 if !ok {
815 continue
816 }
817 content = append(content, fantasy.ToolCallContent{
818 ToolCallID: toolUse.ID,
819 ToolName: toolUse.Name,
820 Input: string(toolUse.Input),
821 ProviderExecuted: false,
822 })
823 }
824 }
825
826 return &fantasy.Response{
827 Content: content,
828 Usage: fantasy.Usage{
829 InputTokens: response.Usage.InputTokens,
830 OutputTokens: response.Usage.OutputTokens,
831 TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
832 CacheCreationTokens: response.Usage.CacheCreationInputTokens,
833 CacheReadTokens: response.Usage.CacheReadInputTokens,
834 },
835 FinishReason: mapFinishReason(string(response.StopReason)),
836 ProviderMetadata: fantasy.ProviderMetadata{},
837 Warnings: warnings,
838 }, nil
839}
840
841// Stream implements fantasy.LanguageModel.
842func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
843 params, warnings, err := a.prepareParams(call)
844 if err != nil {
845 return nil, err
846 }
847
848 stream := a.client.Messages.NewStreaming(ctx, *params)
849 acc := anthropic.Message{}
850 return func(yield func(fantasy.StreamPart) bool) {
851 if len(warnings) > 0 {
852 if !yield(fantasy.StreamPart{
853 Type: fantasy.StreamPartTypeWarnings,
854 Warnings: warnings,
855 }) {
856 return
857 }
858 }
859
860 for stream.Next() {
861 chunk := stream.Current()
862 _ = acc.Accumulate(chunk)
863 switch chunk.Type {
864 case "content_block_start":
865 contentBlockType := chunk.ContentBlock.Type
866 switch contentBlockType {
867 case "text":
868 if !yield(fantasy.StreamPart{
869 Type: fantasy.StreamPartTypeTextStart,
870 ID: fmt.Sprintf("%d", chunk.Index),
871 }) {
872 return
873 }
874 case "thinking":
875 if !yield(fantasy.StreamPart{
876 Type: fantasy.StreamPartTypeReasoningStart,
877 ID: fmt.Sprintf("%d", chunk.Index),
878 }) {
879 return
880 }
881 case "redacted_thinking":
882 if !yield(fantasy.StreamPart{
883 Type: fantasy.StreamPartTypeReasoningStart,
884 ID: fmt.Sprintf("%d", chunk.Index),
885 ProviderMetadata: fantasy.ProviderMetadata{
886 Name: &ReasoningOptionMetadata{
887 RedactedData: chunk.ContentBlock.Data,
888 },
889 },
890 }) {
891 return
892 }
893 case "tool_use":
894 if !yield(fantasy.StreamPart{
895 Type: fantasy.StreamPartTypeToolInputStart,
896 ID: chunk.ContentBlock.ID,
897 ToolCallName: chunk.ContentBlock.Name,
898 ToolCallInput: "",
899 }) {
900 return
901 }
902 }
903 case "content_block_stop":
904 if len(acc.Content)-1 < int(chunk.Index) {
905 continue
906 }
907 contentBlock := acc.Content[int(chunk.Index)]
908 switch contentBlock.Type {
909 case "text":
910 if !yield(fantasy.StreamPart{
911 Type: fantasy.StreamPartTypeTextEnd,
912 ID: fmt.Sprintf("%d", chunk.Index),
913 }) {
914 return
915 }
916 case "thinking":
917 if !yield(fantasy.StreamPart{
918 Type: fantasy.StreamPartTypeReasoningEnd,
919 ID: fmt.Sprintf("%d", chunk.Index),
920 }) {
921 return
922 }
923 case "tool_use":
924 if !yield(fantasy.StreamPart{
925 Type: fantasy.StreamPartTypeToolInputEnd,
926 ID: contentBlock.ID,
927 }) {
928 return
929 }
930 if !yield(fantasy.StreamPart{
931 Type: fantasy.StreamPartTypeToolCall,
932 ID: contentBlock.ID,
933 ToolCallName: contentBlock.Name,
934 ToolCallInput: string(contentBlock.Input),
935 }) {
936 return
937 }
938 }
939 case "content_block_delta":
940 switch chunk.Delta.Type {
941 case "text_delta":
942 if !yield(fantasy.StreamPart{
943 Type: fantasy.StreamPartTypeTextDelta,
944 ID: fmt.Sprintf("%d", chunk.Index),
945 Delta: chunk.Delta.Text,
946 }) {
947 return
948 }
949 case "thinking_delta":
950 if !yield(fantasy.StreamPart{
951 Type: fantasy.StreamPartTypeReasoningDelta,
952 ID: fmt.Sprintf("%d", chunk.Index),
953 Delta: chunk.Delta.Thinking,
954 }) {
955 return
956 }
957 case "signature_delta":
958 if !yield(fantasy.StreamPart{
959 Type: fantasy.StreamPartTypeReasoningDelta,
960 ID: fmt.Sprintf("%d", chunk.Index),
961 ProviderMetadata: fantasy.ProviderMetadata{
962 Name: &ReasoningOptionMetadata{
963 Signature: chunk.Delta.Signature,
964 },
965 },
966 }) {
967 return
968 }
969 case "input_json_delta":
970 if len(acc.Content)-1 < int(chunk.Index) {
971 continue
972 }
973 contentBlock := acc.Content[int(chunk.Index)]
974 if !yield(fantasy.StreamPart{
975 Type: fantasy.StreamPartTypeToolInputDelta,
976 ID: contentBlock.ID,
977 ToolCallInput: chunk.Delta.PartialJSON,
978 }) {
979 return
980 }
981 }
982 case "message_stop":
983 }
984 }
985
986 err := stream.Err()
987 if err == nil || errors.Is(err, io.EOF) {
988 yield(fantasy.StreamPart{
989 Type: fantasy.StreamPartTypeFinish,
990 ID: acc.ID,
991 FinishReason: mapFinishReason(string(acc.StopReason)),
992 Usage: fantasy.Usage{
993 InputTokens: acc.Usage.InputTokens,
994 OutputTokens: acc.Usage.OutputTokens,
995 TotalTokens: acc.Usage.InputTokens + acc.Usage.OutputTokens,
996 CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
997 CacheReadTokens: acc.Usage.CacheReadInputTokens,
998 },
999 ProviderMetadata: fantasy.ProviderMetadata{},
1000 })
1001 return
1002 } else { //nolint: revive
1003 yield(fantasy.StreamPart{
1004 Type: fantasy.StreamPartTypeError,
1005 Error: toProviderErr(err),
1006 })
1007 return
1008 }
1009 }, nil
1010}
1011
1012// GenerateObject implements fantasy.LanguageModel.
1013func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1014 switch a.options.objectMode {
1015 case fantasy.ObjectModeText:
1016 return object.GenerateWithText(ctx, a, call)
1017 default:
1018 return object.GenerateWithTool(ctx, a, call)
1019 }
1020}
1021
1022// StreamObject implements fantasy.LanguageModel.
1023func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1024 switch a.options.objectMode {
1025 case fantasy.ObjectModeText:
1026 return object.StreamWithText(ctx, a, call)
1027 default:
1028 return object.StreamWithTool(ctx, a, call)
1029 }
1030}