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