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