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