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