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