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