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 "math"
14 "strings"
15
16 "charm.land/fantasy"
17 "charm.land/fantasy/object"
18 "charm.land/fantasy/providers/internal/httpheaders"
19 "github.com/aws/aws-sdk-go-v2/config"
20 "github.com/charmbracelet/anthropic-sdk-go"
21 "github.com/charmbracelet/anthropic-sdk-go/bedrock"
22 "github.com/charmbracelet/anthropic-sdk-go/option"
23 "github.com/charmbracelet/anthropic-sdk-go/packages/param"
24 "github.com/charmbracelet/anthropic-sdk-go/vertex"
25 "golang.org/x/oauth2/google"
26)
27
28const (
29 // Name is the name of the Anthropic provider.
30 Name = "anthropic"
31 // DefaultURL is the default URL for the Anthropic API.
32 DefaultURL = "https://api.anthropic.com"
33)
34
35type options struct {
36 baseURL string
37 apiKey string
38 name string
39 headers map[string]string
40 userAgent string
41 client option.HTTPClient
42
43 vertexProject string
44 vertexLocation string
45 skipAuth bool
46
47 useBedrock bool
48
49 objectMode fantasy.ObjectMode
50}
51
52type provider struct {
53 options options
54}
55
56// Option defines a function that configures Anthropic provider options.
57type Option = func(*options)
58
59// New creates a new Anthropic provider with the given options.
60func New(opts ...Option) (fantasy.Provider, error) {
61 providerOptions := options{
62 headers: map[string]string{},
63 objectMode: fantasy.ObjectModeAuto,
64 }
65 for _, o := range opts {
66 o(&providerOptions)
67 }
68
69 providerOptions.baseURL = cmp.Or(providerOptions.baseURL, DefaultURL)
70 providerOptions.name = cmp.Or(providerOptions.name, Name)
71 return &provider{options: providerOptions}, nil
72}
73
74// WithBaseURL sets the base URL for the Anthropic provider.
75func WithBaseURL(baseURL string) Option {
76 return func(o *options) {
77 o.baseURL = baseURL
78 }
79}
80
81// WithAPIKey sets the API key for the Anthropic provider.
82func WithAPIKey(apiKey string) Option {
83 return func(o *options) {
84 o.apiKey = apiKey
85 }
86}
87
88// WithVertex configures the Anthropic provider to use Vertex AI.
89func WithVertex(project, location string) Option {
90 return func(o *options) {
91 o.vertexProject = project
92 o.vertexLocation = location
93 }
94}
95
96// WithSkipAuth configures whether to skip authentication for the Anthropic provider.
97func WithSkipAuth(skip bool) Option {
98 return func(o *options) {
99 o.skipAuth = skip
100 }
101}
102
103// WithBedrock configures the Anthropic provider to use AWS Bedrock.
104func WithBedrock() Option {
105 return func(o *options) {
106 o.useBedrock = true
107 }
108}
109
110// WithName sets the name for the Anthropic provider.
111func WithName(name string) Option {
112 return func(o *options) {
113 o.name = name
114 }
115}
116
117// WithHeaders sets the headers for the Anthropic provider.
118func WithHeaders(headers map[string]string) Option {
119 return func(o *options) {
120 maps.Copy(o.headers, headers)
121 }
122}
123
124// WithHTTPClient sets the HTTP client for the Anthropic provider.
125func WithHTTPClient(client option.HTTPClient) Option {
126 return func(o *options) {
127 o.client = client
128 }
129}
130
131// WithUserAgent sets an explicit User-Agent header, overriding the default and any
132// value set via WithHeaders.
133func WithUserAgent(ua string) Option {
134 return func(o *options) {
135 o.userAgent = ua
136 }
137}
138
139// WithObjectMode sets the object generation mode.
140func WithObjectMode(om fantasy.ObjectMode) Option {
141 return func(o *options) {
142 // not supported
143 if om == fantasy.ObjectModeJSON {
144 om = fantasy.ObjectModeAuto
145 }
146 o.objectMode = om
147 }
148}
149
150func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.LanguageModel, error) {
151 clientOptions := make([]option.RequestOption, 0, 5+len(a.options.headers))
152 clientOptions = append(clientOptions, option.WithMaxRetries(0))
153
154 if a.options.apiKey != "" && !a.options.useBedrock {
155 clientOptions = append(clientOptions, option.WithAPIKey(a.options.apiKey))
156 }
157 if a.options.baseURL != "" {
158 clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL))
159 }
160 defaultUA := httpheaders.DefaultUserAgent(fantasy.Version)
161 resolved := httpheaders.ResolveHeaders(a.options.headers, a.options.userAgent, defaultUA)
162 for key, value := range resolved {
163 clientOptions = append(clientOptions, option.WithHeader(key, value))
164 }
165 if a.options.client != nil {
166 clientOptions = append(clientOptions, option.WithHTTPClient(a.options.client))
167 }
168 if a.options.vertexProject != "" && a.options.vertexLocation != "" {
169 var credentials *google.Credentials
170 if a.options.skipAuth {
171 credentials = &google.Credentials{TokenSource: &googleDummyTokenSource{}}
172 } else {
173 var err error
174 credentials, err = google.FindDefaultCredentials(ctx)
175 if err != nil {
176 return nil, err
177 }
178 }
179
180 clientOptions = append(
181 clientOptions,
182 vertex.WithCredentials(
183 ctx,
184 a.options.vertexLocation,
185 a.options.vertexProject,
186 credentials,
187 ),
188 )
189 }
190 if a.options.useBedrock {
191 modelID = bedrockPrefixModelWithRegion(modelID)
192
193 if a.options.skipAuth || a.options.apiKey != "" {
194 clientOptions = append(
195 clientOptions,
196 bedrock.WithConfig(bedrockBasicAuthConfig(a.options.apiKey)),
197 )
198 } else {
199 if cfg, err := config.LoadDefaultConfig(ctx); err == nil {
200 clientOptions = append(
201 clientOptions,
202 bedrock.WithConfig(cfg),
203 )
204 }
205 }
206 }
207 return languageModel{
208 modelID: modelID,
209 provider: a.options.name,
210 options: a.options,
211 client: anthropic.NewClient(clientOptions...),
212 }, nil
213}
214
215type languageModel struct {
216 provider string
217 modelID string
218 client anthropic.Client
219 options options
220}
221
222// Model implements fantasy.LanguageModel.
223func (a languageModel) Model() string {
224 return a.modelID
225}
226
227// Provider implements fantasy.LanguageModel.
228func (a languageModel) Provider() string {
229 return a.provider
230}
231
232func (a languageModel) prepareParams(call fantasy.Call) (*anthropic.MessageNewParams, []fantasy.CallWarning, error) {
233 params := &anthropic.MessageNewParams{}
234 providerOptions := &ProviderOptions{}
235 if v, ok := call.ProviderOptions[Name]; ok {
236 providerOptions, ok = v.(*ProviderOptions)
237 if !ok {
238 return nil, nil, &fantasy.Error{Title: "invalid argument", Message: "anthropic provider options should be *anthropic.ProviderOptions"}
239 }
240 }
241 sendReasoning := true
242 if providerOptions.SendReasoning != nil {
243 sendReasoning = *providerOptions.SendReasoning
244 }
245 systemBlocks, messages, warnings := toPrompt(call.Prompt, sendReasoning)
246
247 if call.FrequencyPenalty != nil {
248 warnings = append(warnings, fantasy.CallWarning{
249 Type: fantasy.CallWarningTypeUnsupportedSetting,
250 Setting: "FrequencyPenalty",
251 })
252 }
253 if call.PresencePenalty != nil {
254 warnings = append(warnings, fantasy.CallWarning{
255 Type: fantasy.CallWarningTypeUnsupportedSetting,
256 Setting: "PresencePenalty",
257 })
258 }
259
260 params.System = systemBlocks
261 params.Messages = messages
262 params.Model = anthropic.Model(a.modelID)
263 params.MaxTokens = 4096
264
265 if call.MaxOutputTokens != nil {
266 params.MaxTokens = *call.MaxOutputTokens
267 }
268
269 if call.Temperature != nil {
270 params.Temperature = param.NewOpt(*call.Temperature)
271 }
272 if call.TopK != nil {
273 params.TopK = param.NewOpt(*call.TopK)
274 }
275 if call.TopP != nil {
276 params.TopP = param.NewOpt(*call.TopP)
277 }
278
279 switch {
280 case providerOptions.Effort != nil:
281 effort := *providerOptions.Effort
282 params.OutputConfig = anthropic.OutputConfigParam{
283 Effort: anthropic.OutputConfigEffort(effort),
284 }
285 adaptive := anthropic.NewThinkingConfigAdaptiveParam()
286 params.Thinking.OfAdaptive = &adaptive
287 case providerOptions.Thinking != nil:
288 if providerOptions.Thinking.BudgetTokens == 0 {
289 return nil, nil, &fantasy.Error{Title: "no budget", Message: "thinking requires budget"}
290 }
291 params.Thinking = anthropic.ThinkingConfigParamOfEnabled(providerOptions.Thinking.BudgetTokens)
292 if call.Temperature != nil {
293 params.Temperature = param.Opt[float64]{}
294 warnings = append(warnings, fantasy.CallWarning{
295 Type: fantasy.CallWarningTypeUnsupportedSetting,
296 Setting: "temperature",
297 Details: "temperature is not supported when thinking is enabled",
298 })
299 }
300 if call.TopP != nil {
301 params.TopP = param.Opt[float64]{}
302 warnings = append(warnings, fantasy.CallWarning{
303 Type: fantasy.CallWarningTypeUnsupportedSetting,
304 Setting: "TopP",
305 Details: "TopP is not supported when thinking is enabled",
306 })
307 }
308 if call.TopK != nil {
309 params.TopK = param.Opt[int64]{}
310 warnings = append(warnings, fantasy.CallWarning{
311 Type: fantasy.CallWarningTypeUnsupportedSetting,
312 Setting: "TopK",
313 Details: "TopK is not supported when thinking is enabled",
314 })
315 }
316 }
317
318 if len(call.Tools) > 0 {
319 disableParallelToolUse := false
320 if providerOptions.DisableParallelToolUse != nil {
321 disableParallelToolUse = *providerOptions.DisableParallelToolUse
322 }
323 tools, toolChoice, toolWarnings := a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse)
324 params.Tools = tools
325 if toolChoice != nil {
326 params.ToolChoice = *toolChoice
327 }
328 warnings = append(warnings, toolWarnings...)
329 }
330
331 return params, warnings, nil
332}
333
334func (a *provider) Name() string {
335 return Name
336}
337
338// GetCacheControl extracts cache control settings from provider options.
339func GetCacheControl(providerOptions fantasy.ProviderOptions) *CacheControl {
340 if anthropicOptions, ok := providerOptions[Name]; ok {
341 if options, ok := anthropicOptions.(*ProviderCacheControlOptions); ok {
342 return &options.CacheControl
343 }
344 }
345 return nil
346}
347
348// GetReasoningMetadata extracts reasoning metadata from provider options.
349func GetReasoningMetadata(providerOptions fantasy.ProviderOptions) *ReasoningOptionMetadata {
350 if anthropicOptions, ok := providerOptions[Name]; ok {
351 if reasoning, ok := anthropicOptions.(*ReasoningOptionMetadata); ok {
352 return reasoning
353 }
354 }
355 return nil
356}
357
358type messageBlock struct {
359 Role fantasy.MessageRole
360 Messages []fantasy.Message
361}
362
363func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock {
364 var blocks []*messageBlock
365
366 var currentBlock *messageBlock
367
368 for _, msg := range prompt {
369 switch msg.Role {
370 case fantasy.MessageRoleSystem:
371 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleSystem {
372 currentBlock = &messageBlock{
373 Role: fantasy.MessageRoleSystem,
374 Messages: []fantasy.Message{},
375 }
376 blocks = append(blocks, currentBlock)
377 }
378 currentBlock.Messages = append(currentBlock.Messages, msg)
379 case fantasy.MessageRoleUser:
380 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
381 currentBlock = &messageBlock{
382 Role: fantasy.MessageRoleUser,
383 Messages: []fantasy.Message{},
384 }
385 blocks = append(blocks, currentBlock)
386 }
387 currentBlock.Messages = append(currentBlock.Messages, msg)
388 case fantasy.MessageRoleAssistant:
389 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleAssistant {
390 currentBlock = &messageBlock{
391 Role: fantasy.MessageRoleAssistant,
392 Messages: []fantasy.Message{},
393 }
394 blocks = append(blocks, currentBlock)
395 }
396 currentBlock.Messages = append(currentBlock.Messages, msg)
397 case fantasy.MessageRoleTool:
398 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
399 currentBlock = &messageBlock{
400 Role: fantasy.MessageRoleUser,
401 Messages: []fantasy.Message{},
402 }
403 blocks = append(blocks, currentBlock)
404 }
405 currentBlock.Messages = append(currentBlock.Messages, msg)
406 }
407 }
408 return blocks
409}
410
411func anyToStringSlice(v any) []string {
412 switch typed := v.(type) {
413 case []string:
414 if len(typed) == 0 {
415 return nil
416 }
417 out := make([]string, len(typed))
418 copy(out, typed)
419 return out
420 case []any:
421 if len(typed) == 0 {
422 return nil
423 }
424 out := make([]string, 0, len(typed))
425 for _, item := range typed {
426 s, ok := item.(string)
427 if !ok || s == "" {
428 continue
429 }
430 out = append(out, s)
431 }
432 if len(out) == 0 {
433 return nil
434 }
435 return out
436 default:
437 return nil
438 }
439}
440
441const maxExactIntFloat64 = float64(1<<53 - 1)
442
443func anyToInt64(v any) (int64, bool) {
444 switch typed := v.(type) {
445 case int:
446 return int64(typed), true
447 case int8:
448 return int64(typed), true
449 case int16:
450 return int64(typed), true
451 case int32:
452 return int64(typed), true
453 case int64:
454 return typed, true
455 case uint:
456 u64 := uint64(typed)
457 if u64 > math.MaxInt64 {
458 return 0, false
459 }
460 return int64(u64), true
461 case uint8:
462 return int64(typed), true
463 case uint16:
464 return int64(typed), true
465 case uint32:
466 return int64(typed), true
467 case uint64:
468 if typed > math.MaxInt64 {
469 return 0, false
470 }
471 return int64(typed), true
472 case float32:
473 f := float64(typed)
474 if math.Trunc(f) != f || math.IsNaN(f) || math.IsInf(f, 0) || f < -maxExactIntFloat64 || f > maxExactIntFloat64 {
475 return 0, false
476 }
477 return int64(f), true
478 case float64:
479 if math.Trunc(typed) != typed || math.IsNaN(typed) || math.IsInf(typed, 0) || typed < -maxExactIntFloat64 || typed > maxExactIntFloat64 {
480 return 0, false
481 }
482 return int64(typed), true
483 case json.Number:
484 parsed, err := typed.Int64()
485 if err != nil {
486 return 0, false
487 }
488 return parsed, true
489 default:
490 return 0, false
491 }
492}
493
494func anyToUserLocation(v any) *UserLocation {
495 switch typed := v.(type) {
496 case *UserLocation:
497 return typed
498 case UserLocation:
499 loc := typed
500 return &loc
501 case map[string]any:
502 loc := &UserLocation{}
503 if city, ok := typed["city"].(string); ok {
504 loc.City = city
505 }
506 if region, ok := typed["region"].(string); ok {
507 loc.Region = region
508 }
509 if country, ok := typed["country"].(string); ok {
510 loc.Country = country
511 }
512 if timezone, ok := typed["timezone"].(string); ok {
513 loc.Timezone = timezone
514 }
515 if loc.City == "" && loc.Region == "" && loc.Country == "" && loc.Timezone == "" {
516 return nil
517 }
518 return loc
519 default:
520 return nil
521 }
522}
523
524func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (anthropicTools []anthropic.ToolUnionParam, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning) {
525 for _, tool := range tools {
526 if tool.GetType() == fantasy.ToolTypeFunction {
527 ft, ok := tool.(fantasy.FunctionTool)
528 if !ok {
529 continue
530 }
531 required := []string{}
532 var properties any
533 if props, ok := ft.InputSchema["properties"]; ok {
534 properties = props
535 }
536 if req, ok := ft.InputSchema["required"]; ok {
537 if reqArr, ok := req.([]string); ok {
538 required = reqArr
539 }
540 }
541 cacheControl := GetCacheControl(ft.ProviderOptions)
542
543 anthropicTool := anthropic.ToolParam{
544 Name: ft.Name,
545 Description: anthropic.String(ft.Description),
546 InputSchema: anthropic.ToolInputSchemaParam{
547 Properties: properties,
548 Required: required,
549 },
550 }
551 if cacheControl != nil {
552 anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
553 }
554 anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{OfTool: &anthropicTool})
555 continue
556 }
557 if tool.GetType() == fantasy.ToolTypeProviderDefined {
558 pt, ok := tool.(fantasy.ProviderDefinedTool)
559 if !ok {
560 continue
561 }
562 switch pt.ID {
563 case "web_search":
564 webSearchTool := anthropic.WebSearchTool20250305Param{}
565 if pt.Args != nil {
566 if domains := anyToStringSlice(pt.Args["allowed_domains"]); len(domains) > 0 {
567 webSearchTool.AllowedDomains = domains
568 }
569 if domains := anyToStringSlice(pt.Args["blocked_domains"]); len(domains) > 0 {
570 webSearchTool.BlockedDomains = domains
571 }
572 if maxUses, ok := anyToInt64(pt.Args["max_uses"]); ok && maxUses > 0 {
573 webSearchTool.MaxUses = param.NewOpt(maxUses)
574 }
575 if loc := anyToUserLocation(pt.Args["user_location"]); loc != nil {
576 var ulp anthropic.UserLocationParam
577 if loc.City != "" {
578 ulp.City = param.NewOpt(loc.City)
579 }
580 if loc.Region != "" {
581 ulp.Region = param.NewOpt(loc.Region)
582 }
583 if loc.Country != "" {
584 ulp.Country = param.NewOpt(loc.Country)
585 }
586 if loc.Timezone != "" {
587 ulp.Timezone = param.NewOpt(loc.Timezone)
588 }
589 webSearchTool.UserLocation = ulp
590 }
591 }
592 anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{
593 OfWebSearchTool20250305: &webSearchTool,
594 })
595 continue
596 }
597 }
598 warnings = append(warnings, fantasy.CallWarning{
599 Type: fantasy.CallWarningTypeUnsupportedTool,
600 Tool: tool,
601 Message: "tool is not supported",
602 })
603 }
604
605 // NOTE: Bedrock does not support this attribute.
606 var disableParallelToolUse param.Opt[bool]
607 if !a.options.useBedrock {
608 disableParallelToolUse = param.NewOpt(disableParallelToolCalls)
609 }
610
611 if toolChoice == nil {
612 if disableParallelToolCalls {
613 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
614 OfAuto: &anthropic.ToolChoiceAutoParam{
615 Type: "auto",
616 DisableParallelToolUse: disableParallelToolUse,
617 },
618 }
619 }
620 return anthropicTools, anthropicToolChoice, warnings
621 }
622
623 switch *toolChoice {
624 case fantasy.ToolChoiceAuto:
625 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
626 OfAuto: &anthropic.ToolChoiceAutoParam{
627 Type: "auto",
628 DisableParallelToolUse: disableParallelToolUse,
629 },
630 }
631 case fantasy.ToolChoiceRequired:
632 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
633 OfAny: &anthropic.ToolChoiceAnyParam{
634 Type: "any",
635 DisableParallelToolUse: disableParallelToolUse,
636 },
637 }
638 case fantasy.ToolChoiceNone:
639 return anthropicTools, anthropicToolChoice, warnings
640 default:
641 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
642 OfTool: &anthropic.ToolChoiceToolParam{
643 Type: "tool",
644 Name: string(*toolChoice),
645 DisableParallelToolUse: disableParallelToolUse,
646 },
647 }
648 }
649 return anthropicTools, anthropicToolChoice, warnings
650}
651
652func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) {
653 var systemBlocks []anthropic.TextBlockParam
654 var messages []anthropic.MessageParam
655 var warnings []fantasy.CallWarning
656
657 blocks := groupIntoBlocks(prompt)
658 finishedSystemBlock := false
659 for _, block := range blocks {
660 switch block.Role {
661 case fantasy.MessageRoleSystem:
662 if finishedSystemBlock {
663 // skip multiple system messages that are separated by user/assistant messages
664 // TODO: see if we need to send error here?
665 continue
666 }
667 finishedSystemBlock = true
668 for _, msg := range block.Messages {
669 for i, part := range msg.Content {
670 isLastPart := i == len(msg.Content)-1
671 cacheControl := GetCacheControl(part.Options())
672 if cacheControl == nil && isLastPart {
673 cacheControl = GetCacheControl(msg.ProviderOptions)
674 }
675 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
676 if !ok {
677 continue
678 }
679 textBlock := anthropic.TextBlockParam{
680 Text: text.Text,
681 }
682 if cacheControl != nil {
683 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
684 }
685 systemBlocks = append(systemBlocks, textBlock)
686 }
687 }
688
689 case fantasy.MessageRoleUser:
690 var anthropicContent []anthropic.ContentBlockParamUnion
691 for _, msg := range block.Messages {
692 if msg.Role == fantasy.MessageRoleUser {
693 for i, part := range msg.Content {
694 isLastPart := i == len(msg.Content)-1
695 cacheControl := GetCacheControl(part.Options())
696 if cacheControl == nil && isLastPart {
697 cacheControl = GetCacheControl(msg.ProviderOptions)
698 }
699 switch part.GetType() {
700 case fantasy.ContentTypeText:
701 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
702 if !ok {
703 continue
704 }
705 textBlock := &anthropic.TextBlockParam{
706 Text: text.Text,
707 }
708 if cacheControl != nil {
709 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
710 }
711 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
712 OfText: textBlock,
713 })
714 case fantasy.ContentTypeSource:
715 // Source content from web search results is not a
716 // recognized Anthropic content block type; skip it.
717 continue
718 case fantasy.ContentTypeFile:
719 file, ok := fantasy.AsMessagePart[fantasy.FilePart](part)
720 if !ok {
721 continue
722 }
723 // TODO: handle other file types
724 if !strings.HasPrefix(file.MediaType, "image/") {
725 continue
726 }
727
728 base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
729 imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
730 if cacheControl != nil {
731 imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
732 }
733 anthropicContent = append(anthropicContent, imageBlock)
734 }
735 }
736 } else if msg.Role == fantasy.MessageRoleTool {
737 for i, part := range msg.Content {
738 isLastPart := i == len(msg.Content)-1
739 cacheControl := GetCacheControl(part.Options())
740 if cacheControl == nil && isLastPart {
741 cacheControl = GetCacheControl(msg.ProviderOptions)
742 }
743 result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
744 if !ok {
745 continue
746 }
747 toolResultBlock := anthropic.ToolResultBlockParam{
748 ToolUseID: result.ToolCallID,
749 }
750 switch result.Output.GetType() {
751 case fantasy.ToolResultContentTypeText:
752 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
753 if !ok {
754 continue
755 }
756 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
757 {
758 OfText: &anthropic.TextBlockParam{
759 Text: content.Text,
760 },
761 },
762 }
763 case fantasy.ToolResultContentTypeMedia:
764 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
765 if !ok {
766 continue
767 }
768 contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
769 {
770 OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
771 },
772 }
773 if content.Text != "" {
774 contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
775 OfText: &anthropic.TextBlockParam{
776 Text: content.Text,
777 },
778 })
779 }
780 toolResultBlock.Content = contentBlocks
781 case fantasy.ToolResultContentTypeError:
782 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
783 if !ok {
784 continue
785 }
786 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
787 {
788 OfText: &anthropic.TextBlockParam{
789 Text: content.Error.Error(),
790 },
791 },
792 }
793 toolResultBlock.IsError = param.NewOpt(true)
794 }
795 if cacheControl != nil {
796 toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
797 }
798 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
799 OfToolResult: &toolResultBlock,
800 })
801 }
802 }
803 }
804 if !hasVisibleUserContent(anthropicContent) {
805 warnings = append(warnings, fantasy.CallWarning{
806 Type: fantasy.CallWarningTypeOther,
807 Message: "dropping empty user message (contains neither user-facing content nor tool results)",
808 })
809 continue
810 }
811 messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
812 case fantasy.MessageRoleAssistant:
813 var anthropicContent []anthropic.ContentBlockParamUnion
814 for _, msg := range block.Messages {
815 for i, part := range msg.Content {
816 isLastPart := i == len(msg.Content)-1
817 cacheControl := GetCacheControl(part.Options())
818 if cacheControl == nil && isLastPart {
819 cacheControl = GetCacheControl(msg.ProviderOptions)
820 }
821 switch part.GetType() {
822 case fantasy.ContentTypeText:
823 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
824 if !ok {
825 continue
826 }
827 textBlock := &anthropic.TextBlockParam{
828 Text: text.Text,
829 }
830 if cacheControl != nil {
831 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
832 }
833 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
834 OfText: textBlock,
835 })
836 case fantasy.ContentTypeReasoning:
837 reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
838 if !ok {
839 continue
840 }
841 if !sendReasoningData {
842 warnings = append(warnings, fantasy.CallWarning{
843 Type: fantasy.CallWarningTypeOther,
844 Message: "sending reasoning content is disabled for this model",
845 })
846 continue
847 }
848 reasoningMetadata := GetReasoningMetadata(part.Options())
849 if reasoningMetadata == nil {
850 warnings = append(warnings, fantasy.CallWarning{
851 Type: fantasy.CallWarningTypeOther,
852 Message: "unsupported reasoning metadata",
853 })
854 continue
855 }
856
857 if reasoningMetadata.Signature != "" {
858 anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
859 } else if reasoningMetadata.RedactedData != "" {
860 anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
861 } else {
862 warnings = append(warnings, fantasy.CallWarning{
863 Type: fantasy.CallWarningTypeOther,
864 Message: "unsupported reasoning metadata",
865 })
866 continue
867 }
868 case fantasy.ContentTypeToolCall:
869 toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
870 if !ok {
871 continue
872 }
873 if toolCall.ProviderExecuted {
874 // Reconstruct server_tool_use block for
875 // multi-turn round-tripping.
876 var inputAny any
877 err := json.Unmarshal([]byte(toolCall.Input), &inputAny)
878 if err != nil {
879 continue
880 }
881 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
882 OfServerToolUse: &anthropic.ServerToolUseBlockParam{
883 ID: toolCall.ToolCallID,
884 Name: anthropic.ServerToolUseBlockParamName(toolCall.ToolName),
885 Input: inputAny,
886 },
887 })
888 continue
889 }
890 var inputMap map[string]any
891 err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
892 if err != nil {
893 continue
894 }
895 toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
896 if cacheControl != nil {
897 toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
898 }
899 anthropicContent = append(anthropicContent, toolUseBlock)
900 case fantasy.ContentTypeToolResult:
901 result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
902 if !ok {
903 continue
904 }
905 if result.ProviderExecuted {
906 // Reconstruct web_search_tool_result block
907 // with encrypted_content for round-tripping.
908 searchMeta := &WebSearchResultMetadata{}
909 if webMeta, ok := result.ProviderOptions[Name]; ok {
910 if typed, ok := webMeta.(*WebSearchResultMetadata); ok {
911 searchMeta = typed
912 }
913 }
914 anthropicContent = append(anthropicContent, buildWebSearchToolResultBlock(result.ToolCallID, searchMeta))
915 continue
916 }
917 case fantasy.ContentTypeSource: // Source content from web search results is not a
918 // recognized Anthropic content block type; skip it.
919 continue
920 }
921 }
922 }
923 if !hasVisibleAssistantContent(anthropicContent) {
924 warnings = append(warnings, fantasy.CallWarning{
925 Type: fantasy.CallWarningTypeOther,
926 Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
927 })
928 continue
929 }
930 messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
931 }
932 }
933 return systemBlocks, messages, warnings
934}
935
936func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
937 for _, block := range content {
938 if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
939 return true
940 }
941 }
942 return false
943}
944
945func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
946 for _, block := range content {
947 if block.OfText != nil || block.OfToolUse != nil || block.OfServerToolUse != nil || block.OfWebSearchToolResult != nil {
948 return true
949 }
950 }
951 return false
952}
953
954// buildWebSearchToolResultBlock constructs an Anthropic
955// web_search_tool_result content block from structured metadata.
956func buildWebSearchToolResultBlock(toolCallID string, searchMeta *WebSearchResultMetadata) anthropic.ContentBlockParamUnion {
957 resultBlocks := make([]anthropic.WebSearchResultBlockParam, 0, len(searchMeta.Results))
958 for _, r := range searchMeta.Results {
959 block := anthropic.WebSearchResultBlockParam{
960 URL: r.URL,
961 Title: r.Title,
962 EncryptedContent: r.EncryptedContent,
963 }
964 if r.PageAge != "" {
965 block.PageAge = param.NewOpt(r.PageAge)
966 }
967 resultBlocks = append(resultBlocks, block)
968 }
969 return anthropic.ContentBlockParamUnion{
970 OfWebSearchToolResult: &anthropic.WebSearchToolResultBlockParam{
971 ToolUseID: toolCallID,
972 Content: anthropic.WebSearchToolResultBlockParamContentUnion{
973 OfWebSearchToolResultBlockItem: resultBlocks,
974 },
975 },
976 }
977}
978
979func mapFinishReason(finishReason string) fantasy.FinishReason {
980 switch finishReason {
981 case "end_turn", "pause_turn", "stop_sequence":
982 return fantasy.FinishReasonStop
983 case "max_tokens":
984 return fantasy.FinishReasonLength
985 case "tool_use":
986 return fantasy.FinishReasonToolCalls
987 default:
988 return fantasy.FinishReasonUnknown
989 }
990}
991
992// Generate implements fantasy.LanguageModel.
993func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
994 params, warnings, err := a.prepareParams(call)
995 if err != nil {
996 return nil, err
997 }
998 response, err := a.client.Messages.New(ctx, *params, callUARequestOptions(call)...)
999 if err != nil {
1000 return nil, toProviderErr(err)
1001 }
1002
1003 var content []fantasy.Content
1004 for _, block := range response.Content {
1005 switch block.Type {
1006 case "text":
1007 text, ok := block.AsAny().(anthropic.TextBlock)
1008 if !ok {
1009 continue
1010 }
1011 content = append(content, fantasy.TextContent{
1012 Text: text.Text,
1013 })
1014 case "thinking":
1015 reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
1016 if !ok {
1017 continue
1018 }
1019 content = append(content, fantasy.ReasoningContent{
1020 Text: reasoning.Thinking,
1021 ProviderMetadata: fantasy.ProviderMetadata{
1022 Name: &ReasoningOptionMetadata{
1023 Signature: reasoning.Signature,
1024 },
1025 },
1026 })
1027 case "redacted_thinking":
1028 reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
1029 if !ok {
1030 continue
1031 }
1032 content = append(content, fantasy.ReasoningContent{
1033 Text: "",
1034 ProviderMetadata: fantasy.ProviderMetadata{
1035 Name: &ReasoningOptionMetadata{
1036 RedactedData: reasoning.Data,
1037 },
1038 },
1039 })
1040 case "tool_use":
1041 toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
1042 if !ok {
1043 continue
1044 }
1045 content = append(content, fantasy.ToolCallContent{
1046 ToolCallID: toolUse.ID,
1047 ToolName: toolUse.Name,
1048 Input: string(toolUse.Input),
1049 ProviderExecuted: false,
1050 })
1051 case "server_tool_use":
1052 serverToolUse, ok := block.AsAny().(anthropic.ServerToolUseBlock)
1053 if !ok {
1054 continue
1055 }
1056 var inputStr string
1057 if b, err := json.Marshal(serverToolUse.Input); err == nil {
1058 inputStr = string(b)
1059 }
1060 content = append(content, fantasy.ToolCallContent{
1061 ToolCallID: serverToolUse.ID,
1062 ToolName: string(serverToolUse.Name),
1063 Input: inputStr,
1064 ProviderExecuted: true,
1065 })
1066 case "web_search_tool_result":
1067 webSearchResult, ok := block.AsAny().(anthropic.WebSearchToolResultBlock)
1068 if !ok {
1069 continue
1070 }
1071 // Extract search results as sources/citations, preserving
1072 // encrypted_content for multi-turn round-tripping.
1073 toolResult := fantasy.ToolResultContent{
1074 ToolCallID: webSearchResult.ToolUseID,
1075 ToolName: "web_search",
1076 ProviderExecuted: true,
1077 }
1078 if items := webSearchResult.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1079 var metadataResults []WebSearchResultItem
1080 for _, item := range items {
1081 content = append(content, fantasy.SourceContent{
1082 SourceType: fantasy.SourceTypeURL,
1083 ID: item.URL,
1084 URL: item.URL,
1085 Title: item.Title,
1086 })
1087 metadataResults = append(metadataResults, WebSearchResultItem{
1088 URL: item.URL,
1089 Title: item.Title,
1090 EncryptedContent: item.EncryptedContent,
1091 PageAge: item.PageAge,
1092 })
1093 }
1094 toolResult.ProviderMetadata = fantasy.ProviderMetadata{
1095 Name: &WebSearchResultMetadata{
1096 Results: metadataResults,
1097 },
1098 }
1099 }
1100 content = append(content, toolResult)
1101 }
1102 }
1103
1104 return &fantasy.Response{
1105 Content: content,
1106 Usage: fantasy.Usage{
1107 InputTokens: response.Usage.InputTokens,
1108 OutputTokens: response.Usage.OutputTokens,
1109 TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
1110 CacheCreationTokens: response.Usage.CacheCreationInputTokens,
1111 CacheReadTokens: response.Usage.CacheReadInputTokens,
1112 },
1113 FinishReason: mapFinishReason(string(response.StopReason)),
1114 ProviderMetadata: fantasy.ProviderMetadata{},
1115 Warnings: warnings,
1116 }, nil
1117}
1118
1119// Stream implements fantasy.LanguageModel.
1120func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
1121 params, warnings, err := a.prepareParams(call)
1122 if err != nil {
1123 return nil, err
1124 }
1125
1126 stream := a.client.Messages.NewStreaming(ctx, *params, callUARequestOptions(call)...)
1127 acc := anthropic.Message{}
1128 return func(yield func(fantasy.StreamPart) bool) {
1129 if len(warnings) > 0 {
1130 if !yield(fantasy.StreamPart{
1131 Type: fantasy.StreamPartTypeWarnings,
1132 Warnings: warnings,
1133 }) {
1134 return
1135 }
1136 }
1137
1138 for stream.Next() {
1139 chunk := stream.Current()
1140 _ = acc.Accumulate(chunk)
1141 switch chunk.Type {
1142 case "content_block_start":
1143 contentBlockType := chunk.ContentBlock.Type
1144 switch contentBlockType {
1145 case "text":
1146 if !yield(fantasy.StreamPart{
1147 Type: fantasy.StreamPartTypeTextStart,
1148 ID: fmt.Sprintf("%d", chunk.Index),
1149 }) {
1150 return
1151 }
1152 case "thinking":
1153 if !yield(fantasy.StreamPart{
1154 Type: fantasy.StreamPartTypeReasoningStart,
1155 ID: fmt.Sprintf("%d", chunk.Index),
1156 }) {
1157 return
1158 }
1159 case "redacted_thinking":
1160 if !yield(fantasy.StreamPart{
1161 Type: fantasy.StreamPartTypeReasoningStart,
1162 ID: fmt.Sprintf("%d", chunk.Index),
1163 ProviderMetadata: fantasy.ProviderMetadata{
1164 Name: &ReasoningOptionMetadata{
1165 RedactedData: chunk.ContentBlock.Data,
1166 },
1167 },
1168 }) {
1169 return
1170 }
1171 case "tool_use":
1172 if !yield(fantasy.StreamPart{
1173 Type: fantasy.StreamPartTypeToolInputStart,
1174 ID: chunk.ContentBlock.ID,
1175 ToolCallName: chunk.ContentBlock.Name,
1176 ToolCallInput: "",
1177 }) {
1178 return
1179 }
1180 case "server_tool_use":
1181 if !yield(fantasy.StreamPart{
1182 Type: fantasy.StreamPartTypeToolInputStart,
1183 ID: chunk.ContentBlock.ID,
1184 ToolCallName: chunk.ContentBlock.Name,
1185 ToolCallInput: "",
1186 ProviderExecuted: true,
1187 }) {
1188 return
1189 }
1190 }
1191 case "content_block_stop":
1192 if len(acc.Content)-1 < int(chunk.Index) {
1193 continue
1194 }
1195 contentBlock := acc.Content[int(chunk.Index)]
1196 switch contentBlock.Type {
1197 case "text":
1198 if !yield(fantasy.StreamPart{
1199 Type: fantasy.StreamPartTypeTextEnd,
1200 ID: fmt.Sprintf("%d", chunk.Index),
1201 }) {
1202 return
1203 }
1204 case "thinking":
1205 if !yield(fantasy.StreamPart{
1206 Type: fantasy.StreamPartTypeReasoningEnd,
1207 ID: fmt.Sprintf("%d", chunk.Index),
1208 }) {
1209 return
1210 }
1211 case "tool_use":
1212 if !yield(fantasy.StreamPart{
1213 Type: fantasy.StreamPartTypeToolInputEnd,
1214 ID: contentBlock.ID,
1215 }) {
1216 return
1217 }
1218 if !yield(fantasy.StreamPart{
1219 Type: fantasy.StreamPartTypeToolCall,
1220 ID: contentBlock.ID,
1221 ToolCallName: contentBlock.Name,
1222 ToolCallInput: string(contentBlock.Input),
1223 }) {
1224 return
1225 }
1226 case "server_tool_use":
1227 if !yield(fantasy.StreamPart{
1228 Type: fantasy.StreamPartTypeToolInputEnd,
1229 ID: contentBlock.ID,
1230 ProviderExecuted: true,
1231 }) {
1232 return
1233 }
1234 if !yield(fantasy.StreamPart{
1235 Type: fantasy.StreamPartTypeToolCall,
1236 ID: contentBlock.ID,
1237 ToolCallName: contentBlock.Name,
1238 ToolCallInput: string(contentBlock.Input),
1239 ProviderExecuted: true,
1240 }) {
1241 return
1242 }
1243 case "web_search_tool_result":
1244 // Read search results directly from the ContentBlockUnion
1245 // struct fields instead of using AsAny(). The Anthropic SDK's
1246 // Accumulate re-marshals the content block at content_block_stop,
1247 // which corrupts JSON.raw for inline union types like
1248 // WebSearchToolResultBlockContentUnion. The struct fields
1249 // themselves remain correctly populated from content_block_start.
1250 var metadataResults []WebSearchResultItem
1251 var providerMeta fantasy.ProviderMetadata
1252 if items := contentBlock.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1253 for _, item := range items {
1254 if !yield(fantasy.StreamPart{
1255 Type: fantasy.StreamPartTypeSource,
1256 ID: item.URL,
1257 SourceType: fantasy.SourceTypeURL,
1258 URL: item.URL,
1259 Title: item.Title,
1260 }) {
1261 return
1262 }
1263 metadataResults = append(metadataResults, WebSearchResultItem{
1264 URL: item.URL,
1265 Title: item.Title,
1266 EncryptedContent: item.EncryptedContent,
1267 PageAge: item.PageAge,
1268 })
1269 }
1270 }
1271 if len(metadataResults) > 0 {
1272 providerMeta = fantasy.ProviderMetadata{
1273 Name: &WebSearchResultMetadata{
1274 Results: metadataResults,
1275 },
1276 }
1277 }
1278 if !yield(fantasy.StreamPart{
1279 Type: fantasy.StreamPartTypeToolResult,
1280 ID: contentBlock.ToolUseID,
1281 ToolCallName: "web_search",
1282 ProviderExecuted: true,
1283 ProviderMetadata: providerMeta,
1284 }) {
1285 return
1286 }
1287 }
1288 case "content_block_delta":
1289 switch chunk.Delta.Type {
1290 case "text_delta":
1291 if !yield(fantasy.StreamPart{
1292 Type: fantasy.StreamPartTypeTextDelta,
1293 ID: fmt.Sprintf("%d", chunk.Index),
1294 Delta: chunk.Delta.Text,
1295 }) {
1296 return
1297 }
1298 case "thinking_delta":
1299 if !yield(fantasy.StreamPart{
1300 Type: fantasy.StreamPartTypeReasoningDelta,
1301 ID: fmt.Sprintf("%d", chunk.Index),
1302 Delta: chunk.Delta.Thinking,
1303 }) {
1304 return
1305 }
1306 case "signature_delta":
1307 if !yield(fantasy.StreamPart{
1308 Type: fantasy.StreamPartTypeReasoningDelta,
1309 ID: fmt.Sprintf("%d", chunk.Index),
1310 ProviderMetadata: fantasy.ProviderMetadata{
1311 Name: &ReasoningOptionMetadata{
1312 Signature: chunk.Delta.Signature,
1313 },
1314 },
1315 }) {
1316 return
1317 }
1318 case "input_json_delta":
1319 if len(acc.Content)-1 < int(chunk.Index) {
1320 continue
1321 }
1322 contentBlock := acc.Content[int(chunk.Index)]
1323 if !yield(fantasy.StreamPart{
1324 Type: fantasy.StreamPartTypeToolInputDelta,
1325 ID: contentBlock.ID,
1326 ToolCallInput: chunk.Delta.PartialJSON,
1327 }) {
1328 return
1329 }
1330 }
1331 case "message_stop":
1332 }
1333 }
1334
1335 err := stream.Err()
1336 if err == nil || errors.Is(err, io.EOF) {
1337 yield(fantasy.StreamPart{
1338 Type: fantasy.StreamPartTypeFinish,
1339 ID: acc.ID,
1340 FinishReason: mapFinishReason(string(acc.StopReason)),
1341 Usage: fantasy.Usage{
1342 InputTokens: acc.Usage.InputTokens,
1343 OutputTokens: acc.Usage.OutputTokens,
1344 TotalTokens: acc.Usage.InputTokens + acc.Usage.OutputTokens,
1345 CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
1346 CacheReadTokens: acc.Usage.CacheReadInputTokens,
1347 },
1348 ProviderMetadata: fantasy.ProviderMetadata{},
1349 })
1350 return
1351 } else { //nolint: revive
1352 yield(fantasy.StreamPart{
1353 Type: fantasy.StreamPartTypeError,
1354 Error: toProviderErr(err),
1355 })
1356 return
1357 }
1358 }, nil
1359}
1360
1361// GenerateObject implements fantasy.LanguageModel.
1362func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1363 switch a.options.objectMode {
1364 case fantasy.ObjectModeText:
1365 return object.GenerateWithText(ctx, a, call)
1366 default:
1367 return object.GenerateWithTool(ctx, a, call)
1368 }
1369}
1370
1371// StreamObject implements fantasy.LanguageModel.
1372func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1373 switch a.options.objectMode {
1374 case fantasy.ObjectModeText:
1375 return object.StreamWithText(ctx, a, call)
1376 default:
1377 return object.StreamWithTool(ctx, a, call)
1378 }
1379}