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 // TODO: handle other file types
835 switch {
836 case strings.HasPrefix(file.MediaType, "image/"):
837 base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
838 imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
839 if cacheControl != nil {
840 imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
841 }
842 anthropicContent = append(anthropicContent, imageBlock)
843 case strings.HasPrefix(file.MediaType, "text/"):
844 documentBlock := anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{
845 Data: string(file.Data),
846 })
847 anthropicContent = append(anthropicContent, documentBlock)
848 }
849 }
850 }
851 } else if msg.Role == fantasy.MessageRoleTool {
852 for i, part := range msg.Content {
853 isLastPart := i == len(msg.Content)-1
854 cacheControl := GetCacheControl(part.Options())
855 if cacheControl == nil && isLastPart {
856 cacheControl = GetCacheControl(msg.ProviderOptions)
857 }
858 result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
859 if !ok {
860 continue
861 }
862 toolResultBlock := anthropic.ToolResultBlockParam{
863 ToolUseID: result.ToolCallID,
864 }
865 switch result.Output.GetType() {
866 case fantasy.ToolResultContentTypeText:
867 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
868 if !ok {
869 continue
870 }
871 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
872 {
873 OfText: &anthropic.TextBlockParam{
874 Text: content.Text,
875 },
876 },
877 }
878 case fantasy.ToolResultContentTypeMedia:
879 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
880 if !ok {
881 continue
882 }
883 contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
884 {
885 OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
886 },
887 }
888 if content.Text != "" {
889 contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
890 OfText: &anthropic.TextBlockParam{
891 Text: content.Text,
892 },
893 })
894 }
895 toolResultBlock.Content = contentBlocks
896 case fantasy.ToolResultContentTypeError:
897 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
898 if !ok {
899 continue
900 }
901 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
902 {
903 OfText: &anthropic.TextBlockParam{
904 Text: content.Error.Error(),
905 },
906 },
907 }
908 toolResultBlock.IsError = param.NewOpt(true)
909 }
910 if cacheControl != nil {
911 toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
912 }
913 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
914 OfToolResult: &toolResultBlock,
915 })
916 }
917 }
918 }
919 if !hasVisibleUserContent(anthropicContent) {
920 warnings = append(warnings, fantasy.CallWarning{
921 Type: fantasy.CallWarningTypeOther,
922 Message: "dropping empty user message (contains neither user-facing content nor tool results)",
923 })
924 continue
925 }
926 messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
927 case fantasy.MessageRoleAssistant:
928 var anthropicContent []anthropic.ContentBlockParamUnion
929 for _, msg := range block.Messages {
930 for i, part := range msg.Content {
931 isLastPart := i == len(msg.Content)-1
932 cacheControl := GetCacheControl(part.Options())
933 if cacheControl == nil && isLastPart {
934 cacheControl = GetCacheControl(msg.ProviderOptions)
935 }
936 switch part.GetType() {
937 case fantasy.ContentTypeText:
938 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
939 if !ok {
940 continue
941 }
942 textBlock := &anthropic.TextBlockParam{
943 Text: text.Text,
944 }
945 if cacheControl != nil {
946 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
947 }
948 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
949 OfText: textBlock,
950 })
951 case fantasy.ContentTypeReasoning:
952 reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
953 if !ok {
954 continue
955 }
956 if !sendReasoningData {
957 warnings = append(warnings, fantasy.CallWarning{
958 Type: fantasy.CallWarningTypeOther,
959 Message: "sending reasoning content is disabled for this model",
960 })
961 continue
962 }
963 reasoningMetadata := GetReasoningMetadata(part.Options())
964 if reasoningMetadata == nil {
965 warnings = append(warnings, fantasy.CallWarning{
966 Type: fantasy.CallWarningTypeOther,
967 Message: "unsupported reasoning metadata",
968 })
969 continue
970 }
971
972 if reasoningMetadata.Signature != "" {
973 anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
974 } else if reasoningMetadata.RedactedData != "" {
975 anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
976 } else {
977 warnings = append(warnings, fantasy.CallWarning{
978 Type: fantasy.CallWarningTypeOther,
979 Message: "unsupported reasoning metadata",
980 })
981 continue
982 }
983 case fantasy.ContentTypeToolCall:
984 toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
985 if !ok {
986 continue
987 }
988 if toolCall.ProviderExecuted {
989 // Reconstruct server_tool_use block for
990 // multi-turn round-tripping.
991 var inputAny any
992 err := json.Unmarshal([]byte(toolCall.Input), &inputAny)
993 if err != nil {
994 continue
995 }
996 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
997 OfServerToolUse: &anthropic.ServerToolUseBlockParam{
998 ID: toolCall.ToolCallID,
999 Name: anthropic.ServerToolUseBlockParamName(toolCall.ToolName),
1000 Input: inputAny,
1001 },
1002 })
1003 continue
1004 }
1005 var inputMap map[string]any
1006 err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
1007 if err != nil {
1008 continue
1009 }
1010 toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
1011 if cacheControl != nil {
1012 toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
1013 }
1014 anthropicContent = append(anthropicContent, toolUseBlock)
1015 case fantasy.ContentTypeToolResult:
1016 result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
1017 if !ok {
1018 continue
1019 }
1020 if result.ProviderExecuted {
1021 // Reconstruct web_search_tool_result block
1022 // with encrypted_content for round-tripping.
1023 searchMeta := &WebSearchResultMetadata{}
1024 if webMeta, ok := result.ProviderOptions[Name]; ok {
1025 if typed, ok := webMeta.(*WebSearchResultMetadata); ok {
1026 searchMeta = typed
1027 }
1028 }
1029 anthropicContent = append(anthropicContent, buildWebSearchToolResultBlock(result.ToolCallID, searchMeta))
1030 continue
1031 }
1032 case fantasy.ContentTypeSource: // Source content from web search results is not a
1033 // recognized Anthropic content block type; skip it.
1034 continue
1035 }
1036 }
1037 }
1038 if !hasVisibleAssistantContent(anthropicContent) {
1039 warnings = append(warnings, fantasy.CallWarning{
1040 Type: fantasy.CallWarningTypeOther,
1041 Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
1042 })
1043 continue
1044 }
1045 messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
1046 }
1047 }
1048 return systemBlocks, messages, warnings
1049}
1050
1051func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
1052 for _, block := range content {
1053 if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
1054 return true
1055 }
1056 }
1057 return false
1058}
1059
1060func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
1061 for _, block := range content {
1062 if block.OfText != nil || block.OfToolUse != nil || block.OfServerToolUse != nil || block.OfWebSearchToolResult != nil {
1063 return true
1064 }
1065 }
1066 return false
1067}
1068
1069// buildWebSearchToolResultBlock constructs an Anthropic
1070// web_search_tool_result content block from structured metadata.
1071func buildWebSearchToolResultBlock(toolCallID string, searchMeta *WebSearchResultMetadata) anthropic.ContentBlockParamUnion {
1072 resultBlocks := make([]anthropic.WebSearchResultBlockParam, 0, len(searchMeta.Results))
1073 for _, r := range searchMeta.Results {
1074 block := anthropic.WebSearchResultBlockParam{
1075 URL: r.URL,
1076 Title: r.Title,
1077 EncryptedContent: r.EncryptedContent,
1078 }
1079 if r.PageAge != "" {
1080 block.PageAge = param.NewOpt(r.PageAge)
1081 }
1082 resultBlocks = append(resultBlocks, block)
1083 }
1084 return anthropic.ContentBlockParamUnion{
1085 OfWebSearchToolResult: &anthropic.WebSearchToolResultBlockParam{
1086 ToolUseID: toolCallID,
1087 Content: anthropic.WebSearchToolResultBlockParamContentUnion{
1088 OfWebSearchToolResultBlockItem: resultBlocks,
1089 },
1090 },
1091 }
1092}
1093
1094func mapFinishReason(finishReason string) fantasy.FinishReason {
1095 switch finishReason {
1096 case "end_turn", "pause_turn", "stop_sequence":
1097 return fantasy.FinishReasonStop
1098 case "max_tokens":
1099 return fantasy.FinishReasonLength
1100 case "tool_use":
1101 return fantasy.FinishReasonToolCalls
1102 default:
1103 return fantasy.FinishReasonUnknown
1104 }
1105}
1106
1107// Generate implements fantasy.LanguageModel.
1108func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
1109 params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
1110 if err != nil {
1111 return nil, err
1112 }
1113 reqOpts := buildRequestOptions(call, rawTools, betaFlags)
1114
1115 response, err := a.client.Messages.New(ctx, *params, reqOpts...)
1116 if err != nil {
1117 return nil, toProviderErr(err)
1118 }
1119
1120 var content []fantasy.Content
1121 for _, block := range response.Content {
1122 switch block.Type {
1123 case "text":
1124 text, ok := block.AsAny().(anthropic.TextBlock)
1125 if !ok {
1126 continue
1127 }
1128 content = append(content, fantasy.TextContent{
1129 Text: text.Text,
1130 })
1131 case "thinking":
1132 reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
1133 if !ok {
1134 continue
1135 }
1136 content = append(content, fantasy.ReasoningContent{
1137 Text: reasoning.Thinking,
1138 ProviderMetadata: fantasy.ProviderMetadata{
1139 Name: &ReasoningOptionMetadata{
1140 Signature: reasoning.Signature,
1141 },
1142 },
1143 })
1144 case "redacted_thinking":
1145 reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
1146 if !ok {
1147 continue
1148 }
1149 content = append(content, fantasy.ReasoningContent{
1150 Text: "",
1151 ProviderMetadata: fantasy.ProviderMetadata{
1152 Name: &ReasoningOptionMetadata{
1153 RedactedData: reasoning.Data,
1154 },
1155 },
1156 })
1157 case "tool_use":
1158 toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
1159 if !ok {
1160 continue
1161 }
1162 content = append(content, fantasy.ToolCallContent{
1163 ToolCallID: toolUse.ID,
1164 ToolName: toolUse.Name,
1165 Input: string(toolUse.Input),
1166 ProviderExecuted: false,
1167 })
1168 case "server_tool_use":
1169 serverToolUse, ok := block.AsAny().(anthropic.ServerToolUseBlock)
1170 if !ok {
1171 continue
1172 }
1173 var inputStr string
1174 if b, err := json.Marshal(serverToolUse.Input); err == nil {
1175 inputStr = string(b)
1176 }
1177 content = append(content, fantasy.ToolCallContent{
1178 ToolCallID: serverToolUse.ID,
1179 ToolName: string(serverToolUse.Name),
1180 Input: inputStr,
1181 ProviderExecuted: true,
1182 })
1183 case "web_search_tool_result":
1184 webSearchResult, ok := block.AsAny().(anthropic.WebSearchToolResultBlock)
1185 if !ok {
1186 continue
1187 }
1188 // Extract search results as sources/citations, preserving
1189 // encrypted_content for multi-turn round-tripping.
1190 toolResult := fantasy.ToolResultContent{
1191 ToolCallID: webSearchResult.ToolUseID,
1192 ToolName: "web_search",
1193 ProviderExecuted: true,
1194 }
1195 if items := webSearchResult.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1196 var metadataResults []WebSearchResultItem
1197 for _, item := range items {
1198 content = append(content, fantasy.SourceContent{
1199 SourceType: fantasy.SourceTypeURL,
1200 ID: item.URL,
1201 URL: item.URL,
1202 Title: item.Title,
1203 })
1204 metadataResults = append(metadataResults, WebSearchResultItem{
1205 URL: item.URL,
1206 Title: item.Title,
1207 EncryptedContent: item.EncryptedContent,
1208 PageAge: item.PageAge,
1209 })
1210 }
1211 toolResult.ProviderMetadata = fantasy.ProviderMetadata{
1212 Name: &WebSearchResultMetadata{
1213 Results: metadataResults,
1214 },
1215 }
1216 }
1217 content = append(content, toolResult)
1218 }
1219 }
1220
1221 return &fantasy.Response{
1222 Content: content,
1223 Usage: fantasy.Usage{
1224 InputTokens: response.Usage.InputTokens,
1225 OutputTokens: response.Usage.OutputTokens,
1226 TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
1227 CacheCreationTokens: response.Usage.CacheCreationInputTokens,
1228 CacheReadTokens: response.Usage.CacheReadInputTokens,
1229 },
1230 FinishReason: mapFinishReason(string(response.StopReason)),
1231 ProviderMetadata: fantasy.ProviderMetadata{},
1232 Warnings: warnings,
1233 }, nil
1234}
1235
1236// Stream implements fantasy.LanguageModel.
1237func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
1238 params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
1239 if err != nil {
1240 return nil, err
1241 }
1242
1243 reqOpts := buildRequestOptions(call, rawTools, betaFlags)
1244
1245 stream := a.client.Messages.NewStreaming(ctx, *params, reqOpts...)
1246 acc := anthropic.Message{}
1247 return func(yield func(fantasy.StreamPart) bool) {
1248 if len(warnings) > 0 {
1249 if !yield(fantasy.StreamPart{
1250 Type: fantasy.StreamPartTypeWarnings,
1251 Warnings: warnings,
1252 }) {
1253 return
1254 }
1255 }
1256
1257 for stream.Next() {
1258 chunk := stream.Current()
1259 _ = acc.Accumulate(chunk)
1260 switch chunk.Type {
1261 case "content_block_start":
1262 contentBlockType := chunk.ContentBlock.Type
1263 switch contentBlockType {
1264 case "text":
1265 if !yield(fantasy.StreamPart{
1266 Type: fantasy.StreamPartTypeTextStart,
1267 ID: fmt.Sprintf("%d", chunk.Index),
1268 }) {
1269 return
1270 }
1271 case "thinking":
1272 if !yield(fantasy.StreamPart{
1273 Type: fantasy.StreamPartTypeReasoningStart,
1274 ID: fmt.Sprintf("%d", chunk.Index),
1275 }) {
1276 return
1277 }
1278 case "redacted_thinking":
1279 if !yield(fantasy.StreamPart{
1280 Type: fantasy.StreamPartTypeReasoningStart,
1281 ID: fmt.Sprintf("%d", chunk.Index),
1282 ProviderMetadata: fantasy.ProviderMetadata{
1283 Name: &ReasoningOptionMetadata{
1284 RedactedData: chunk.ContentBlock.Data,
1285 },
1286 },
1287 }) {
1288 return
1289 }
1290 case "tool_use":
1291 if !yield(fantasy.StreamPart{
1292 Type: fantasy.StreamPartTypeToolInputStart,
1293 ID: chunk.ContentBlock.ID,
1294 ToolCallName: chunk.ContentBlock.Name,
1295 ToolCallInput: "",
1296 }) {
1297 return
1298 }
1299 case "server_tool_use":
1300 if !yield(fantasy.StreamPart{
1301 Type: fantasy.StreamPartTypeToolInputStart,
1302 ID: chunk.ContentBlock.ID,
1303 ToolCallName: chunk.ContentBlock.Name,
1304 ToolCallInput: "",
1305 ProviderExecuted: true,
1306 }) {
1307 return
1308 }
1309 }
1310 case "content_block_stop":
1311 if len(acc.Content)-1 < int(chunk.Index) {
1312 continue
1313 }
1314 contentBlock := acc.Content[int(chunk.Index)]
1315 switch contentBlock.Type {
1316 case "text":
1317 if !yield(fantasy.StreamPart{
1318 Type: fantasy.StreamPartTypeTextEnd,
1319 ID: fmt.Sprintf("%d", chunk.Index),
1320 }) {
1321 return
1322 }
1323 case "thinking":
1324 if !yield(fantasy.StreamPart{
1325 Type: fantasy.StreamPartTypeReasoningEnd,
1326 ID: fmt.Sprintf("%d", chunk.Index),
1327 }) {
1328 return
1329 }
1330 case "tool_use":
1331 if !yield(fantasy.StreamPart{
1332 Type: fantasy.StreamPartTypeToolInputEnd,
1333 ID: contentBlock.ID,
1334 }) {
1335 return
1336 }
1337 if !yield(fantasy.StreamPart{
1338 Type: fantasy.StreamPartTypeToolCall,
1339 ID: contentBlock.ID,
1340 ToolCallName: contentBlock.Name,
1341 ToolCallInput: string(contentBlock.Input),
1342 }) {
1343 return
1344 }
1345 case "server_tool_use":
1346 if !yield(fantasy.StreamPart{
1347 Type: fantasy.StreamPartTypeToolInputEnd,
1348 ID: contentBlock.ID,
1349 ProviderExecuted: true,
1350 }) {
1351 return
1352 }
1353 if !yield(fantasy.StreamPart{
1354 Type: fantasy.StreamPartTypeToolCall,
1355 ID: contentBlock.ID,
1356 ToolCallName: contentBlock.Name,
1357 ToolCallInput: string(contentBlock.Input),
1358 ProviderExecuted: true,
1359 }) {
1360 return
1361 }
1362 case "web_search_tool_result":
1363 // Read search results directly from the ContentBlockUnion
1364 // struct fields instead of using AsAny(). The Anthropic SDK's
1365 // Accumulate re-marshals the content block at content_block_stop,
1366 // which corrupts JSON.raw for inline union types like
1367 // WebSearchToolResultBlockContentUnion. The struct fields
1368 // themselves remain correctly populated from content_block_start.
1369 var metadataResults []WebSearchResultItem
1370 var providerMeta fantasy.ProviderMetadata
1371 if items := contentBlock.Content.OfWebSearchResultBlockArray; len(items) > 0 {
1372 for _, item := range items {
1373 if !yield(fantasy.StreamPart{
1374 Type: fantasy.StreamPartTypeSource,
1375 ID: item.URL,
1376 SourceType: fantasy.SourceTypeURL,
1377 URL: item.URL,
1378 Title: item.Title,
1379 }) {
1380 return
1381 }
1382 metadataResults = append(metadataResults, WebSearchResultItem{
1383 URL: item.URL,
1384 Title: item.Title,
1385 EncryptedContent: item.EncryptedContent,
1386 PageAge: item.PageAge,
1387 })
1388 }
1389 }
1390 if len(metadataResults) > 0 {
1391 providerMeta = fantasy.ProviderMetadata{
1392 Name: &WebSearchResultMetadata{
1393 Results: metadataResults,
1394 },
1395 }
1396 }
1397 if !yield(fantasy.StreamPart{
1398 Type: fantasy.StreamPartTypeToolResult,
1399 ID: contentBlock.ToolUseID,
1400 ToolCallName: "web_search",
1401 ProviderExecuted: true,
1402 ProviderMetadata: providerMeta,
1403 }) {
1404 return
1405 }
1406 }
1407 case "content_block_delta":
1408 switch chunk.Delta.Type {
1409 case "text_delta":
1410 if !yield(fantasy.StreamPart{
1411 Type: fantasy.StreamPartTypeTextDelta,
1412 ID: fmt.Sprintf("%d", chunk.Index),
1413 Delta: chunk.Delta.Text,
1414 }) {
1415 return
1416 }
1417 case "thinking_delta":
1418 if !yield(fantasy.StreamPart{
1419 Type: fantasy.StreamPartTypeReasoningDelta,
1420 ID: fmt.Sprintf("%d", chunk.Index),
1421 Delta: chunk.Delta.Thinking,
1422 }) {
1423 return
1424 }
1425 case "signature_delta":
1426 if !yield(fantasy.StreamPart{
1427 Type: fantasy.StreamPartTypeReasoningDelta,
1428 ID: fmt.Sprintf("%d", chunk.Index),
1429 ProviderMetadata: fantasy.ProviderMetadata{
1430 Name: &ReasoningOptionMetadata{
1431 Signature: chunk.Delta.Signature,
1432 },
1433 },
1434 }) {
1435 return
1436 }
1437 case "input_json_delta":
1438 if len(acc.Content)-1 < int(chunk.Index) {
1439 continue
1440 }
1441 contentBlock := acc.Content[int(chunk.Index)]
1442 if !yield(fantasy.StreamPart{
1443 Type: fantasy.StreamPartTypeToolInputDelta,
1444 ID: contentBlock.ID,
1445 ToolCallInput: chunk.Delta.PartialJSON,
1446 }) {
1447 return
1448 }
1449 }
1450 case "message_stop":
1451 }
1452 }
1453
1454 err := stream.Err()
1455 if err == nil || errors.Is(err, io.EOF) {
1456 yield(fantasy.StreamPart{
1457 Type: fantasy.StreamPartTypeFinish,
1458 ID: acc.ID,
1459 FinishReason: mapFinishReason(string(acc.StopReason)),
1460 Usage: fantasy.Usage{
1461 InputTokens: acc.Usage.InputTokens,
1462 OutputTokens: acc.Usage.OutputTokens,
1463 TotalTokens: acc.Usage.InputTokens + acc.Usage.OutputTokens,
1464 CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
1465 CacheReadTokens: acc.Usage.CacheReadInputTokens,
1466 },
1467 ProviderMetadata: fantasy.ProviderMetadata{},
1468 })
1469 return
1470 } else { //nolint: revive
1471 yield(fantasy.StreamPart{
1472 Type: fantasy.StreamPartTypeError,
1473 Error: toProviderErr(err),
1474 })
1475 return
1476 }
1477 }, nil
1478}
1479
1480// GenerateObject implements fantasy.LanguageModel.
1481func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1482 switch a.options.objectMode {
1483 case fantasy.ObjectModeText:
1484 return object.GenerateWithText(ctx, a, call)
1485 default:
1486 return object.GenerateWithTool(ctx, a, call)
1487 }
1488}
1489
1490// StreamObject implements fantasy.LanguageModel.
1491func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1492 switch a.options.objectMode {
1493 case fantasy.ObjectModeText:
1494 return object.StreamWithText(ctx, a, call)
1495 default:
1496 return object.StreamWithTool(ctx, a, call)
1497 }
1498}