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