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 "strings"
14
15 "charm.land/fantasy"
16 "github.com/charmbracelet/anthropic-sdk-go"
17 "github.com/charmbracelet/anthropic-sdk-go/bedrock"
18 "github.com/charmbracelet/anthropic-sdk-go/option"
19 "github.com/charmbracelet/anthropic-sdk-go/packages/param"
20 "github.com/charmbracelet/anthropic-sdk-go/vertex"
21 "golang.org/x/oauth2/google"
22)
23
24const (
25 // Name is the name of the Anthropic provider.
26 Name = "anthropic"
27 // DefaultURL is the default URL for the Anthropic API.
28 DefaultURL = "https://api.anthropic.com"
29)
30
31type options struct {
32 baseURL string
33 apiKey string
34 name string
35 headers map[string]string
36 client option.HTTPClient
37
38 vertexProject string
39 vertexLocation string
40 skipAuth bool
41
42 useBedrock bool
43}
44
45type provider struct {
46 options options
47}
48
49// Option defines a function that configures Anthropic provider options.
50type Option = func(*options)
51
52// New creates a new Anthropic provider with the given options.
53func New(opts ...Option) (fantasy.Provider, error) {
54 providerOptions := options{
55 headers: map[string]string{},
56 }
57 for _, o := range opts {
58 o(&providerOptions)
59 }
60
61 providerOptions.baseURL = cmp.Or(providerOptions.baseURL, DefaultURL)
62 providerOptions.name = cmp.Or(providerOptions.name, Name)
63 return &provider{options: providerOptions}, nil
64}
65
66// WithBaseURL sets the base URL for the Anthropic provider.
67func WithBaseURL(baseURL string) Option {
68 return func(o *options) {
69 o.baseURL = baseURL
70 }
71}
72
73// WithAPIKey sets the API key for the Anthropic provider.
74func WithAPIKey(apiKey string) Option {
75 return func(o *options) {
76 o.apiKey = apiKey
77 }
78}
79
80// WithVertex configures the Anthropic provider to use Vertex AI.
81func WithVertex(project, location string) Option {
82 return func(o *options) {
83 o.vertexProject = project
84 o.vertexLocation = location
85 }
86}
87
88// WithSkipAuth configures whether to skip authentication for the Anthropic provider.
89func WithSkipAuth(skip bool) Option {
90 return func(o *options) {
91 o.skipAuth = skip
92 }
93}
94
95// WithBedrock configures the Anthropic provider to use AWS Bedrock.
96func WithBedrock() Option {
97 return func(o *options) {
98 o.useBedrock = true
99 }
100}
101
102// WithName sets the name for the Anthropic provider.
103func WithName(name string) Option {
104 return func(o *options) {
105 o.name = name
106 }
107}
108
109// WithHeaders sets the headers for the Anthropic provider.
110func WithHeaders(headers map[string]string) Option {
111 return func(o *options) {
112 maps.Copy(o.headers, headers)
113 }
114}
115
116// WithHTTPClient sets the HTTP client for the Anthropic provider.
117func WithHTTPClient(client option.HTTPClient) Option {
118 return func(o *options) {
119 o.client = client
120 }
121}
122
123func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.LanguageModel, error) {
124 clientOptions := make([]option.RequestOption, 0, 5+len(a.options.headers))
125 if a.options.apiKey != "" && !a.options.useBedrock {
126 clientOptions = append(clientOptions, option.WithAPIKey(a.options.apiKey))
127 }
128 if a.options.baseURL != "" {
129 clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL))
130 }
131 for key, value := range a.options.headers {
132 clientOptions = append(clientOptions, option.WithHeader(key, value))
133 }
134 if a.options.client != nil {
135 clientOptions = append(clientOptions, option.WithHTTPClient(a.options.client))
136 }
137 if a.options.vertexProject != "" && a.options.vertexLocation != "" {
138 var credentials *google.Credentials
139 if a.options.skipAuth {
140 credentials = &google.Credentials{TokenSource: &googleDummyTokenSource{}}
141 } else {
142 var err error
143 credentials, err = google.FindDefaultCredentials(ctx)
144 if err != nil {
145 return nil, err
146 }
147 }
148
149 clientOptions = append(
150 clientOptions,
151 vertex.WithCredentials(
152 ctx,
153 a.options.vertexLocation,
154 a.options.vertexProject,
155 credentials,
156 ),
157 )
158 }
159 if a.options.useBedrock {
160 if a.options.skipAuth || a.options.apiKey != "" {
161 clientOptions = append(
162 clientOptions,
163 bedrock.WithConfig(bedrockBasicAuthConfig(a.options.apiKey)),
164 )
165 } else {
166 clientOptions = append(
167 clientOptions,
168 bedrock.WithLoadDefaultConfig(ctx),
169 )
170 }
171 }
172 return languageModel{
173 modelID: modelID,
174 provider: a.options.name,
175 options: a.options,
176 client: anthropic.NewClient(clientOptions...),
177 }, nil
178}
179
180type languageModel struct {
181 provider string
182 modelID string
183 client anthropic.Client
184 options options
185}
186
187// Model implements fantasy.LanguageModel.
188func (a languageModel) Model() string {
189 return a.modelID
190}
191
192// Provider implements fantasy.LanguageModel.
193func (a languageModel) Provider() string {
194 return a.provider
195}
196
197func (a languageModel) prepareParams(call fantasy.Call) (*anthropic.MessageNewParams, []fantasy.CallWarning, error) {
198 params := &anthropic.MessageNewParams{}
199 providerOptions := &ProviderOptions{}
200 if v, ok := call.ProviderOptions[Name]; ok {
201 providerOptions, ok = v.(*ProviderOptions)
202 if !ok {
203 return nil, nil, fantasy.NewInvalidArgumentError("providerOptions", "anthropic provider options should be *anthropic.ProviderOptions", nil)
204 }
205 }
206 sendReasoning := true
207 if providerOptions.SendReasoning != nil {
208 sendReasoning = *providerOptions.SendReasoning
209 }
210 systemBlocks, messages, warnings := toPrompt(call.Prompt, sendReasoning)
211
212 if call.FrequencyPenalty != nil {
213 warnings = append(warnings, fantasy.CallWarning{
214 Type: fantasy.CallWarningTypeUnsupportedSetting,
215 Setting: "FrequencyPenalty",
216 })
217 }
218 if call.PresencePenalty != nil {
219 warnings = append(warnings, fantasy.CallWarning{
220 Type: fantasy.CallWarningTypeUnsupportedSetting,
221 Setting: "PresencePenalty",
222 })
223 }
224
225 params.System = systemBlocks
226 params.Messages = messages
227 params.Model = anthropic.Model(a.modelID)
228 params.MaxTokens = 4096
229
230 if call.MaxOutputTokens != nil {
231 params.MaxTokens = *call.MaxOutputTokens
232 }
233
234 if call.Temperature != nil {
235 params.Temperature = param.NewOpt(*call.Temperature)
236 }
237 if call.TopK != nil {
238 params.TopK = param.NewOpt(*call.TopK)
239 }
240 if call.TopP != nil {
241 params.TopP = param.NewOpt(*call.TopP)
242 }
243
244 isThinking := false
245 var thinkingBudget int64
246 if providerOptions.Thinking != nil {
247 isThinking = true
248 thinkingBudget = providerOptions.Thinking.BudgetTokens
249 }
250 if isThinking {
251 if thinkingBudget == 0 {
252 return nil, nil, fantasy.NewUnsupportedFunctionalityError("thinking requires budget", "")
253 }
254 params.Thinking = anthropic.ThinkingConfigParamOfEnabled(thinkingBudget)
255 if call.Temperature != nil {
256 params.Temperature = param.Opt[float64]{}
257 warnings = append(warnings, fantasy.CallWarning{
258 Type: fantasy.CallWarningTypeUnsupportedSetting,
259 Setting: "temperature",
260 Details: "temperature is not supported when thinking is enabled",
261 })
262 }
263 if call.TopP != nil {
264 params.TopP = param.Opt[float64]{}
265 warnings = append(warnings, fantasy.CallWarning{
266 Type: fantasy.CallWarningTypeUnsupportedSetting,
267 Setting: "TopP",
268 Details: "TopP is not supported when thinking is enabled",
269 })
270 }
271 if call.TopK != nil {
272 params.TopK = param.Opt[int64]{}
273 warnings = append(warnings, fantasy.CallWarning{
274 Type: fantasy.CallWarningTypeUnsupportedSetting,
275 Setting: "TopK",
276 Details: "TopK is not supported when thinking is enabled",
277 })
278 }
279 params.MaxTokens = params.MaxTokens + thinkingBudget
280 }
281
282 if len(call.Tools) > 0 {
283 disableParallelToolUse := false
284 if providerOptions.DisableParallelToolUse != nil {
285 disableParallelToolUse = *providerOptions.DisableParallelToolUse
286 }
287 tools, toolChoice, toolWarnings := a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse)
288 params.Tools = tools
289 if toolChoice != nil {
290 params.ToolChoice = *toolChoice
291 }
292 warnings = append(warnings, toolWarnings...)
293 }
294
295 return params, warnings, nil
296}
297
298func (a *provider) Name() string {
299 return Name
300}
301
302// GetCacheControl extracts cache control settings from provider options.
303func GetCacheControl(providerOptions fantasy.ProviderOptions) *CacheControl {
304 if anthropicOptions, ok := providerOptions[Name]; ok {
305 if options, ok := anthropicOptions.(*ProviderCacheControlOptions); ok {
306 return &options.CacheControl
307 }
308 }
309 return nil
310}
311
312// GetReasoningMetadata extracts reasoning metadata from provider options.
313func GetReasoningMetadata(providerOptions fantasy.ProviderOptions) *ReasoningOptionMetadata {
314 if anthropicOptions, ok := providerOptions[Name]; ok {
315 if reasoning, ok := anthropicOptions.(*ReasoningOptionMetadata); ok {
316 return reasoning
317 }
318 }
319 return nil
320}
321
322type messageBlock struct {
323 Role fantasy.MessageRole
324 Messages []fantasy.Message
325}
326
327func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock {
328 var blocks []*messageBlock
329
330 var currentBlock *messageBlock
331
332 for _, msg := range prompt {
333 switch msg.Role {
334 case fantasy.MessageRoleSystem:
335 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleSystem {
336 currentBlock = &messageBlock{
337 Role: fantasy.MessageRoleSystem,
338 Messages: []fantasy.Message{},
339 }
340 blocks = append(blocks, currentBlock)
341 }
342 currentBlock.Messages = append(currentBlock.Messages, msg)
343 case fantasy.MessageRoleUser:
344 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
345 currentBlock = &messageBlock{
346 Role: fantasy.MessageRoleUser,
347 Messages: []fantasy.Message{},
348 }
349 blocks = append(blocks, currentBlock)
350 }
351 currentBlock.Messages = append(currentBlock.Messages, msg)
352 case fantasy.MessageRoleAssistant:
353 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleAssistant {
354 currentBlock = &messageBlock{
355 Role: fantasy.MessageRoleAssistant,
356 Messages: []fantasy.Message{},
357 }
358 blocks = append(blocks, currentBlock)
359 }
360 currentBlock.Messages = append(currentBlock.Messages, msg)
361 case fantasy.MessageRoleTool:
362 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
363 currentBlock = &messageBlock{
364 Role: fantasy.MessageRoleUser,
365 Messages: []fantasy.Message{},
366 }
367 blocks = append(blocks, currentBlock)
368 }
369 currentBlock.Messages = append(currentBlock.Messages, msg)
370 }
371 }
372 return blocks
373}
374
375func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (anthropicTools []anthropic.ToolUnionParam, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning) {
376 for _, tool := range tools {
377 if tool.GetType() == fantasy.ToolTypeFunction {
378 ft, ok := tool.(fantasy.FunctionTool)
379 if !ok {
380 continue
381 }
382 required := []string{}
383 var properties any
384 if props, ok := ft.InputSchema["properties"]; ok {
385 properties = props
386 }
387 if req, ok := ft.InputSchema["required"]; ok {
388 if reqArr, ok := req.([]string); ok {
389 required = reqArr
390 }
391 }
392 cacheControl := GetCacheControl(ft.ProviderOptions)
393
394 anthropicTool := anthropic.ToolParam{
395 Name: ft.Name,
396 Description: anthropic.String(ft.Description),
397 InputSchema: anthropic.ToolInputSchemaParam{
398 Properties: properties,
399 Required: required,
400 },
401 }
402 if cacheControl != nil {
403 anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
404 }
405 anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{OfTool: &anthropicTool})
406 continue
407 }
408 // TODO: handle provider tool calls
409 warnings = append(warnings, fantasy.CallWarning{
410 Type: fantasy.CallWarningTypeUnsupportedTool,
411 Tool: tool,
412 Message: "tool is not supported",
413 })
414 }
415
416 // NOTE: Bedrock does not support this attribute.
417 var disableParallelToolUse param.Opt[bool]
418 if !a.options.useBedrock {
419 disableParallelToolUse = param.NewOpt(disableParallelToolCalls)
420 }
421
422 if toolChoice == nil {
423 if disableParallelToolCalls {
424 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
425 OfAuto: &anthropic.ToolChoiceAutoParam{
426 Type: "auto",
427 DisableParallelToolUse: disableParallelToolUse,
428 },
429 }
430 }
431 return anthropicTools, anthropicToolChoice, warnings
432 }
433
434 switch *toolChoice {
435 case fantasy.ToolChoiceAuto:
436 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
437 OfAuto: &anthropic.ToolChoiceAutoParam{
438 Type: "auto",
439 DisableParallelToolUse: disableParallelToolUse,
440 },
441 }
442 case fantasy.ToolChoiceRequired:
443 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
444 OfAny: &anthropic.ToolChoiceAnyParam{
445 Type: "any",
446 DisableParallelToolUse: disableParallelToolUse,
447 },
448 }
449 case fantasy.ToolChoiceNone:
450 return anthropicTools, anthropicToolChoice, warnings
451 default:
452 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
453 OfTool: &anthropic.ToolChoiceToolParam{
454 Type: "tool",
455 Name: string(*toolChoice),
456 DisableParallelToolUse: disableParallelToolUse,
457 },
458 }
459 }
460 return anthropicTools, anthropicToolChoice, warnings
461}
462
463func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) {
464 var systemBlocks []anthropic.TextBlockParam
465 var messages []anthropic.MessageParam
466 var warnings []fantasy.CallWarning
467
468 blocks := groupIntoBlocks(prompt)
469 finishedSystemBlock := false
470 for _, block := range blocks {
471 switch block.Role {
472 case fantasy.MessageRoleSystem:
473 if finishedSystemBlock {
474 // skip multiple system messages that are separated by user/assistant messages
475 // TODO: see if we need to send error here?
476 continue
477 }
478 finishedSystemBlock = true
479 for _, msg := range block.Messages {
480 for i, part := range msg.Content {
481 isLastPart := i == len(msg.Content)-1
482 cacheControl := GetCacheControl(part.Options())
483 if cacheControl == nil && isLastPart {
484 cacheControl = GetCacheControl(msg.ProviderOptions)
485 }
486 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
487 if !ok {
488 continue
489 }
490 textBlock := anthropic.TextBlockParam{
491 Text: text.Text,
492 }
493 if cacheControl != nil {
494 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
495 }
496 systemBlocks = append(systemBlocks, textBlock)
497 }
498 }
499
500 case fantasy.MessageRoleUser:
501 var anthropicContent []anthropic.ContentBlockParamUnion
502 for _, msg := range block.Messages {
503 if msg.Role == fantasy.MessageRoleUser {
504 for i, part := range msg.Content {
505 isLastPart := i == len(msg.Content)-1
506 cacheControl := GetCacheControl(part.Options())
507 if cacheControl == nil && isLastPart {
508 cacheControl = GetCacheControl(msg.ProviderOptions)
509 }
510 switch part.GetType() {
511 case fantasy.ContentTypeText:
512 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
513 if !ok {
514 continue
515 }
516 textBlock := &anthropic.TextBlockParam{
517 Text: text.Text,
518 }
519 if cacheControl != nil {
520 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
521 }
522 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
523 OfText: textBlock,
524 })
525 case fantasy.ContentTypeFile:
526 file, ok := fantasy.AsMessagePart[fantasy.FilePart](part)
527 if !ok {
528 continue
529 }
530 // TODO: handle other file types
531 if !strings.HasPrefix(file.MediaType, "image/") {
532 continue
533 }
534
535 base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
536 imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
537 if cacheControl != nil {
538 imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
539 }
540 anthropicContent = append(anthropicContent, imageBlock)
541 }
542 }
543 } else if msg.Role == fantasy.MessageRoleTool {
544 for i, part := range msg.Content {
545 isLastPart := i == len(msg.Content)-1
546 cacheControl := GetCacheControl(part.Options())
547 if cacheControl == nil && isLastPart {
548 cacheControl = GetCacheControl(msg.ProviderOptions)
549 }
550 result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
551 if !ok {
552 continue
553 }
554 toolResultBlock := anthropic.ToolResultBlockParam{
555 ToolUseID: result.ToolCallID,
556 }
557 switch result.Output.GetType() {
558 case fantasy.ToolResultContentTypeText:
559 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
560 if !ok {
561 continue
562 }
563 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
564 {
565 OfText: &anthropic.TextBlockParam{
566 Text: content.Text,
567 },
568 },
569 }
570 case fantasy.ToolResultContentTypeMedia:
571 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
572 if !ok {
573 continue
574 }
575 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
576 {
577 OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
578 },
579 }
580 case fantasy.ToolResultContentTypeError:
581 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
582 if !ok {
583 continue
584 }
585 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
586 {
587 OfText: &anthropic.TextBlockParam{
588 Text: content.Error.Error(),
589 },
590 },
591 }
592 toolResultBlock.IsError = param.NewOpt(true)
593 }
594 if cacheControl != nil {
595 toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
596 }
597 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
598 OfToolResult: &toolResultBlock,
599 })
600 }
601 }
602 }
603 messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
604 case fantasy.MessageRoleAssistant:
605 var anthropicContent []anthropic.ContentBlockParamUnion
606 for _, msg := range block.Messages {
607 for i, part := range msg.Content {
608 isLastPart := i == len(msg.Content)-1
609 cacheControl := GetCacheControl(part.Options())
610 if cacheControl == nil && isLastPart {
611 cacheControl = GetCacheControl(msg.ProviderOptions)
612 }
613 switch part.GetType() {
614 case fantasy.ContentTypeText:
615 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
616 if !ok {
617 continue
618 }
619 textBlock := &anthropic.TextBlockParam{
620 Text: text.Text,
621 }
622 if cacheControl != nil {
623 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
624 }
625 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
626 OfText: textBlock,
627 })
628 case fantasy.ContentTypeReasoning:
629 reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
630 if !ok {
631 continue
632 }
633 if !sendReasoningData {
634 warnings = append(warnings, fantasy.CallWarning{
635 Type: "other",
636 Message: "sending reasoning content is disabled for this model",
637 })
638 continue
639 }
640 reasoningMetadata := GetReasoningMetadata(part.Options())
641 if reasoningMetadata == nil {
642 warnings = append(warnings, fantasy.CallWarning{
643 Type: "other",
644 Message: "unsupported reasoning metadata",
645 })
646 continue
647 }
648
649 if reasoningMetadata.Signature != "" {
650 anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
651 } else if reasoningMetadata.RedactedData != "" {
652 anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
653 } else {
654 warnings = append(warnings, fantasy.CallWarning{
655 Type: "other",
656 Message: "unsupported reasoning metadata",
657 })
658 continue
659 }
660 case fantasy.ContentTypeToolCall:
661 toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
662 if !ok {
663 continue
664 }
665 if toolCall.ProviderExecuted {
666 // TODO: implement provider executed call
667 continue
668 }
669
670 var inputMap map[string]any
671 err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
672 if err != nil {
673 continue
674 }
675 toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
676 if cacheControl != nil {
677 toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
678 }
679 anthropicContent = append(anthropicContent, toolUseBlock)
680 case fantasy.ContentTypeToolResult:
681 // TODO: implement provider executed tool result
682 }
683 }
684 }
685 messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
686 }
687 }
688 return systemBlocks, messages, warnings
689}
690
691func (a languageModel) handleError(err error) error {
692 var apiErr *anthropic.Error
693 if errors.As(err, &apiErr) {
694 requestDump := apiErr.DumpRequest(true)
695 responseDump := apiErr.DumpResponse(true)
696 headers := map[string]string{}
697 for k, h := range apiErr.Response.Header {
698 v := h[len(h)-1]
699 headers[strings.ToLower(k)] = v
700 }
701 return fantasy.NewAPICallError(
702 apiErr.Error(),
703 apiErr.Request.URL.String(),
704 string(requestDump),
705 apiErr.StatusCode,
706 headers,
707 string(responseDump),
708 apiErr,
709 false,
710 )
711 }
712 return err
713}
714
715func mapFinishReason(finishReason string) fantasy.FinishReason {
716 switch finishReason {
717 case "end_turn", "pause_turn", "stop_sequence":
718 return fantasy.FinishReasonStop
719 case "max_tokens":
720 return fantasy.FinishReasonLength
721 case "tool_use":
722 return fantasy.FinishReasonToolCalls
723 default:
724 return fantasy.FinishReasonUnknown
725 }
726}
727
728// Generate implements fantasy.LanguageModel.
729func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
730 params, warnings, err := a.prepareParams(call)
731 if err != nil {
732 return nil, err
733 }
734 response, err := a.client.Messages.New(ctx, *params)
735 if err != nil {
736 return nil, a.handleError(err)
737 }
738
739 var content []fantasy.Content
740 for _, block := range response.Content {
741 switch block.Type {
742 case "text":
743 text, ok := block.AsAny().(anthropic.TextBlock)
744 if !ok {
745 continue
746 }
747 content = append(content, fantasy.TextContent{
748 Text: text.Text,
749 })
750 case "thinking":
751 reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
752 if !ok {
753 continue
754 }
755 content = append(content, fantasy.ReasoningContent{
756 Text: reasoning.Thinking,
757 ProviderMetadata: fantasy.ProviderMetadata{
758 Name: &ReasoningOptionMetadata{
759 Signature: reasoning.Signature,
760 },
761 },
762 })
763 case "redacted_thinking":
764 reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
765 if !ok {
766 continue
767 }
768 content = append(content, fantasy.ReasoningContent{
769 Text: "",
770 ProviderMetadata: fantasy.ProviderMetadata{
771 Name: &ReasoningOptionMetadata{
772 RedactedData: reasoning.Data,
773 },
774 },
775 })
776 case "tool_use":
777 toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
778 if !ok {
779 continue
780 }
781 content = append(content, fantasy.ToolCallContent{
782 ToolCallID: toolUse.ID,
783 ToolName: toolUse.Name,
784 Input: string(toolUse.Input),
785 ProviderExecuted: false,
786 })
787 }
788 }
789
790 return &fantasy.Response{
791 Content: content,
792 Usage: fantasy.Usage{
793 InputTokens: response.Usage.InputTokens,
794 OutputTokens: response.Usage.OutputTokens,
795 TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
796 CacheCreationTokens: response.Usage.CacheCreationInputTokens,
797 CacheReadTokens: response.Usage.CacheReadInputTokens,
798 },
799 FinishReason: mapFinishReason(string(response.StopReason)),
800 ProviderMetadata: fantasy.ProviderMetadata{},
801 Warnings: warnings,
802 }, nil
803}
804
805// Stream implements fantasy.LanguageModel.
806func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
807 params, warnings, err := a.prepareParams(call)
808 if err != nil {
809 return nil, err
810 }
811
812 stream := a.client.Messages.NewStreaming(ctx, *params)
813 acc := anthropic.Message{}
814 return func(yield func(fantasy.StreamPart) bool) {
815 if len(warnings) > 0 {
816 if !yield(fantasy.StreamPart{
817 Type: fantasy.StreamPartTypeWarnings,
818 Warnings: warnings,
819 }) {
820 return
821 }
822 }
823
824 for stream.Next() {
825 chunk := stream.Current()
826 _ = acc.Accumulate(chunk)
827 switch chunk.Type {
828 case "content_block_start":
829 contentBlockType := chunk.ContentBlock.Type
830 switch contentBlockType {
831 case "text":
832 if !yield(fantasy.StreamPart{
833 Type: fantasy.StreamPartTypeTextStart,
834 ID: fmt.Sprintf("%d", chunk.Index),
835 }) {
836 return
837 }
838 case "thinking":
839 if !yield(fantasy.StreamPart{
840 Type: fantasy.StreamPartTypeReasoningStart,
841 ID: fmt.Sprintf("%d", chunk.Index),
842 }) {
843 return
844 }
845 case "redacted_thinking":
846 if !yield(fantasy.StreamPart{
847 Type: fantasy.StreamPartTypeReasoningStart,
848 ID: fmt.Sprintf("%d", chunk.Index),
849 ProviderMetadata: fantasy.ProviderMetadata{
850 Name: &ReasoningOptionMetadata{
851 RedactedData: chunk.ContentBlock.Data,
852 },
853 },
854 }) {
855 return
856 }
857 case "tool_use":
858 if !yield(fantasy.StreamPart{
859 Type: fantasy.StreamPartTypeToolInputStart,
860 ID: chunk.ContentBlock.ID,
861 ToolCallName: chunk.ContentBlock.Name,
862 ToolCallInput: "",
863 }) {
864 return
865 }
866 }
867 case "content_block_stop":
868 if len(acc.Content)-1 < int(chunk.Index) {
869 continue
870 }
871 contentBlock := acc.Content[int(chunk.Index)]
872 switch contentBlock.Type {
873 case "text":
874 if !yield(fantasy.StreamPart{
875 Type: fantasy.StreamPartTypeTextEnd,
876 ID: fmt.Sprintf("%d", chunk.Index),
877 }) {
878 return
879 }
880 case "thinking":
881 if !yield(fantasy.StreamPart{
882 Type: fantasy.StreamPartTypeReasoningEnd,
883 ID: fmt.Sprintf("%d", chunk.Index),
884 }) {
885 return
886 }
887 case "tool_use":
888 if !yield(fantasy.StreamPart{
889 Type: fantasy.StreamPartTypeToolInputEnd,
890 ID: contentBlock.ID,
891 }) {
892 return
893 }
894 if !yield(fantasy.StreamPart{
895 Type: fantasy.StreamPartTypeToolCall,
896 ID: contentBlock.ID,
897 ToolCallName: contentBlock.Name,
898 ToolCallInput: string(contentBlock.Input),
899 }) {
900 return
901 }
902 }
903 case "content_block_delta":
904 switch chunk.Delta.Type {
905 case "text_delta":
906 if !yield(fantasy.StreamPart{
907 Type: fantasy.StreamPartTypeTextDelta,
908 ID: fmt.Sprintf("%d", chunk.Index),
909 Delta: chunk.Delta.Text,
910 }) {
911 return
912 }
913 case "thinking_delta":
914 if !yield(fantasy.StreamPart{
915 Type: fantasy.StreamPartTypeReasoningDelta,
916 ID: fmt.Sprintf("%d", chunk.Index),
917 Delta: chunk.Delta.Thinking,
918 }) {
919 return
920 }
921 case "signature_delta":
922 if !yield(fantasy.StreamPart{
923 Type: fantasy.StreamPartTypeReasoningDelta,
924 ID: fmt.Sprintf("%d", chunk.Index),
925 ProviderMetadata: fantasy.ProviderMetadata{
926 Name: &ReasoningOptionMetadata{
927 Signature: chunk.Delta.Signature,
928 },
929 },
930 }) {
931 return
932 }
933 case "input_json_delta":
934 if len(acc.Content)-1 < int(chunk.Index) {
935 continue
936 }
937 contentBlock := acc.Content[int(chunk.Index)]
938 if !yield(fantasy.StreamPart{
939 Type: fantasy.StreamPartTypeToolInputDelta,
940 ID: contentBlock.ID,
941 ToolCallInput: chunk.Delta.PartialJSON,
942 }) {
943 return
944 }
945 }
946 case "message_stop":
947 }
948 }
949
950 err := stream.Err()
951 if err == nil || errors.Is(err, io.EOF) {
952 yield(fantasy.StreamPart{
953 Type: fantasy.StreamPartTypeFinish,
954 ID: acc.ID,
955 FinishReason: mapFinishReason(string(acc.StopReason)),
956 Usage: fantasy.Usage{
957 InputTokens: acc.Usage.InputTokens,
958 OutputTokens: acc.Usage.OutputTokens,
959 TotalTokens: acc.Usage.InputTokens + acc.Usage.OutputTokens,
960 CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
961 CacheReadTokens: acc.Usage.CacheReadInputTokens,
962 },
963 ProviderMetadata: fantasy.ProviderMetadata{},
964 })
965 return
966 } else { //nolint: revive
967 yield(fantasy.StreamPart{
968 Type: fantasy.StreamPartTypeError,
969 Error: a.handleError(err),
970 })
971 return
972 }
973 }, nil
974}