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/anthropics/anthropic-sdk-go"
17 "github.com/anthropics/anthropic-sdk-go/bedrock"
18 "github.com/anthropics/anthropic-sdk-go/option"
19 "github.com/anthropics/anthropic-sdk-go/packages/param"
20 "github.com/anthropics/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 {
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}
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(modelID string) (fantasy.LanguageModel, error) {
124 clientOptions := make([]option.RequestOption, 0, 5+len(a.options.headers))
125 if a.options.apiKey != "" {
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(context.TODO())
144 if err != nil {
145 return nil, err
146 }
147 }
148
149 clientOptions = append(
150 clientOptions,
151 vertex.WithCredentials(
152 context.TODO(),
153 a.options.vertexLocation,
154 a.options.vertexProject,
155 credentials,
156 ),
157 )
158 }
159 if a.options.useBedrock {
160 if a.options.skipAuth {
161 clientOptions = append(
162 clientOptions,
163 bedrock.WithConfig(dummyBedrockConfig),
164 )
165 } else {
166 clientOptions = append(
167 clientOptions,
168 bedrock.WithLoadDefaultConfig(context.TODO()),
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
302func getCacheControl(providerOptions fantasy.ProviderOptions) *CacheControl {
303 if anthropicOptions, ok := providerOptions[Name]; ok {
304 if options, ok := anthropicOptions.(*ProviderCacheControlOptions); ok {
305 return &options.CacheControl
306 }
307 }
308 return nil
309}
310
311func getReasoningMetadata(providerOptions fantasy.ProviderOptions) *ReasoningOptionMetadata {
312 if anthropicOptions, ok := providerOptions[Name]; ok {
313 if reasoning, ok := anthropicOptions.(*ReasoningOptionMetadata); ok {
314 return reasoning
315 }
316 }
317 return nil
318}
319
320type messageBlock struct {
321 Role fantasy.MessageRole
322 Messages []fantasy.Message
323}
324
325func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock {
326 var blocks []*messageBlock
327
328 var currentBlock *messageBlock
329
330 for _, msg := range prompt {
331 switch msg.Role {
332 case fantasy.MessageRoleSystem:
333 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleSystem {
334 currentBlock = &messageBlock{
335 Role: fantasy.MessageRoleSystem,
336 Messages: []fantasy.Message{},
337 }
338 blocks = append(blocks, currentBlock)
339 }
340 currentBlock.Messages = append(currentBlock.Messages, msg)
341 case fantasy.MessageRoleUser:
342 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
343 currentBlock = &messageBlock{
344 Role: fantasy.MessageRoleUser,
345 Messages: []fantasy.Message{},
346 }
347 blocks = append(blocks, currentBlock)
348 }
349 currentBlock.Messages = append(currentBlock.Messages, msg)
350 case fantasy.MessageRoleAssistant:
351 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleAssistant {
352 currentBlock = &messageBlock{
353 Role: fantasy.MessageRoleAssistant,
354 Messages: []fantasy.Message{},
355 }
356 blocks = append(blocks, currentBlock)
357 }
358 currentBlock.Messages = append(currentBlock.Messages, msg)
359 case fantasy.MessageRoleTool:
360 if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
361 currentBlock = &messageBlock{
362 Role: fantasy.MessageRoleUser,
363 Messages: []fantasy.Message{},
364 }
365 blocks = append(blocks, currentBlock)
366 }
367 currentBlock.Messages = append(currentBlock.Messages, msg)
368 }
369 }
370 return blocks
371}
372
373func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (anthropicTools []anthropic.ToolUnionParam, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning) {
374 for _, tool := range tools {
375 if tool.GetType() == fantasy.ToolTypeFunction {
376 ft, ok := tool.(fantasy.FunctionTool)
377 if !ok {
378 continue
379 }
380 required := []string{}
381 var properties any
382 if props, ok := ft.InputSchema["properties"]; ok {
383 properties = props
384 }
385 if req, ok := ft.InputSchema["required"]; ok {
386 if reqArr, ok := req.([]string); ok {
387 required = reqArr
388 }
389 }
390 cacheControl := getCacheControl(ft.ProviderOptions)
391
392 anthropicTool := anthropic.ToolParam{
393 Name: ft.Name,
394 Description: anthropic.String(ft.Description),
395 InputSchema: anthropic.ToolInputSchemaParam{
396 Properties: properties,
397 Required: required,
398 },
399 }
400 if cacheControl != nil {
401 anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
402 }
403 anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{OfTool: &anthropicTool})
404 continue
405 }
406 // TODO: handle provider tool calls
407 warnings = append(warnings, fantasy.CallWarning{
408 Type: fantasy.CallWarningTypeUnsupportedTool,
409 Tool: tool,
410 Message: "tool is not supported",
411 })
412 }
413
414 // NOTE: Bedrock does not support this attribute.
415 var disableParallelToolUse param.Opt[bool]
416 if !a.options.useBedrock {
417 disableParallelToolUse = param.NewOpt(disableParallelToolCalls)
418 }
419
420 if toolChoice == nil {
421 if disableParallelToolCalls {
422 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
423 OfAuto: &anthropic.ToolChoiceAutoParam{
424 Type: "auto",
425 DisableParallelToolUse: disableParallelToolUse,
426 },
427 }
428 }
429 return anthropicTools, anthropicToolChoice, warnings
430 }
431
432 switch *toolChoice {
433 case fantasy.ToolChoiceAuto:
434 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
435 OfAuto: &anthropic.ToolChoiceAutoParam{
436 Type: "auto",
437 DisableParallelToolUse: disableParallelToolUse,
438 },
439 }
440 case fantasy.ToolChoiceRequired:
441 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
442 OfAny: &anthropic.ToolChoiceAnyParam{
443 Type: "any",
444 DisableParallelToolUse: disableParallelToolUse,
445 },
446 }
447 case fantasy.ToolChoiceNone:
448 return anthropicTools, anthropicToolChoice, warnings
449 default:
450 anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
451 OfTool: &anthropic.ToolChoiceToolParam{
452 Type: "tool",
453 Name: string(*toolChoice),
454 DisableParallelToolUse: disableParallelToolUse,
455 },
456 }
457 }
458 return anthropicTools, anthropicToolChoice, warnings
459}
460
461func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) {
462 var systemBlocks []anthropic.TextBlockParam
463 var messages []anthropic.MessageParam
464 var warnings []fantasy.CallWarning
465
466 blocks := groupIntoBlocks(prompt)
467 finishedSystemBlock := false
468 for _, block := range blocks {
469 switch block.Role {
470 case fantasy.MessageRoleSystem:
471 if finishedSystemBlock {
472 // skip multiple system messages that are separated by user/assistant messages
473 // TODO: see if we need to send error here?
474 continue
475 }
476 finishedSystemBlock = true
477 for _, msg := range block.Messages {
478 for i, part := range msg.Content {
479 isLastPart := i == len(msg.Content)-1
480 cacheControl := getCacheControl(part.Options())
481 if cacheControl == nil && isLastPart {
482 cacheControl = getCacheControl(msg.ProviderOptions)
483 }
484 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
485 if !ok {
486 continue
487 }
488 textBlock := anthropic.TextBlockParam{
489 Text: text.Text,
490 }
491 if cacheControl != nil {
492 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
493 }
494 systemBlocks = append(systemBlocks, textBlock)
495 }
496 }
497
498 case fantasy.MessageRoleUser:
499 var anthropicContent []anthropic.ContentBlockParamUnion
500 for _, msg := range block.Messages {
501 if msg.Role == fantasy.MessageRoleUser {
502 for i, part := range msg.Content {
503 isLastPart := i == len(msg.Content)-1
504 cacheControl := getCacheControl(part.Options())
505 if cacheControl == nil && isLastPart {
506 cacheControl = getCacheControl(msg.ProviderOptions)
507 }
508 switch part.GetType() {
509 case fantasy.ContentTypeText:
510 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
511 if !ok {
512 continue
513 }
514 textBlock := &anthropic.TextBlockParam{
515 Text: text.Text,
516 }
517 if cacheControl != nil {
518 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
519 }
520 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
521 OfText: textBlock,
522 })
523 case fantasy.ContentTypeFile:
524 file, ok := fantasy.AsMessagePart[fantasy.FilePart](part)
525 if !ok {
526 continue
527 }
528 // TODO: handle other file types
529 if !strings.HasPrefix(file.MediaType, "image/") {
530 continue
531 }
532
533 base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
534 imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
535 if cacheControl != nil {
536 imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
537 }
538 anthropicContent = append(anthropicContent, imageBlock)
539 }
540 }
541 } else if msg.Role == fantasy.MessageRoleTool {
542 for i, part := range msg.Content {
543 isLastPart := i == len(msg.Content)-1
544 cacheControl := getCacheControl(part.Options())
545 if cacheControl == nil && isLastPart {
546 cacheControl = getCacheControl(msg.ProviderOptions)
547 }
548 result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
549 if !ok {
550 continue
551 }
552 toolResultBlock := anthropic.ToolResultBlockParam{
553 ToolUseID: result.ToolCallID,
554 }
555 switch result.Output.GetType() {
556 case fantasy.ToolResultContentTypeText:
557 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
558 if !ok {
559 continue
560 }
561 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
562 {
563 OfText: &anthropic.TextBlockParam{
564 Text: content.Text,
565 },
566 },
567 }
568 case fantasy.ToolResultContentTypeMedia:
569 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
570 if !ok {
571 continue
572 }
573 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
574 {
575 OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
576 },
577 }
578 case fantasy.ToolResultContentTypeError:
579 content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
580 if !ok {
581 continue
582 }
583 toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
584 {
585 OfText: &anthropic.TextBlockParam{
586 Text: content.Error.Error(),
587 },
588 },
589 }
590 toolResultBlock.IsError = param.NewOpt(true)
591 }
592 if cacheControl != nil {
593 toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
594 }
595 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
596 OfToolResult: &toolResultBlock,
597 })
598 }
599 }
600 }
601 messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
602 case fantasy.MessageRoleAssistant:
603 var anthropicContent []anthropic.ContentBlockParamUnion
604 for _, msg := range block.Messages {
605 for i, part := range msg.Content {
606 isLastPart := i == len(msg.Content)-1
607 cacheControl := getCacheControl(part.Options())
608 if cacheControl == nil && isLastPart {
609 cacheControl = getCacheControl(msg.ProviderOptions)
610 }
611 switch part.GetType() {
612 case fantasy.ContentTypeText:
613 text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
614 if !ok {
615 continue
616 }
617 textBlock := &anthropic.TextBlockParam{
618 Text: text.Text,
619 }
620 if cacheControl != nil {
621 textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
622 }
623 anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
624 OfText: textBlock,
625 })
626 case fantasy.ContentTypeReasoning:
627 reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
628 if !ok {
629 continue
630 }
631 if !sendReasoningData {
632 warnings = append(warnings, fantasy.CallWarning{
633 Type: "other",
634 Message: "sending reasoning content is disabled for this model",
635 })
636 continue
637 }
638 reasoningMetadata := getReasoningMetadata(part.Options())
639 if reasoningMetadata == nil {
640 warnings = append(warnings, fantasy.CallWarning{
641 Type: "other",
642 Message: "unsupported reasoning metadata",
643 })
644 continue
645 }
646
647 if reasoningMetadata.Signature != "" {
648 anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
649 } else if reasoningMetadata.RedactedData != "" {
650 anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
651 } else {
652 warnings = append(warnings, fantasy.CallWarning{
653 Type: "other",
654 Message: "unsupported reasoning metadata",
655 })
656 continue
657 }
658 case fantasy.ContentTypeToolCall:
659 toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
660 if !ok {
661 continue
662 }
663 if toolCall.ProviderExecuted {
664 // TODO: implement provider executed call
665 continue
666 }
667
668 var inputMap map[string]any
669 err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
670 if err != nil {
671 continue
672 }
673 toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
674 if cacheControl != nil {
675 toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
676 }
677 anthropicContent = append(anthropicContent, toolUseBlock)
678 case fantasy.ContentTypeToolResult:
679 // TODO: implement provider executed tool result
680 }
681 }
682 }
683 messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
684 }
685 }
686 return systemBlocks, messages, warnings
687}
688
689func (a languageModel) handleError(err error) error {
690 var apiErr *anthropic.Error
691 if errors.As(err, &apiErr) {
692 requestDump := apiErr.DumpRequest(true)
693 responseDump := apiErr.DumpResponse(true)
694 headers := map[string]string{}
695 for k, h := range apiErr.Response.Header {
696 v := h[len(h)-1]
697 headers[strings.ToLower(k)] = v
698 }
699 return fantasy.NewAPICallError(
700 apiErr.Error(),
701 apiErr.Request.URL.String(),
702 string(requestDump),
703 apiErr.StatusCode,
704 headers,
705 string(responseDump),
706 apiErr,
707 false,
708 )
709 }
710 return err
711}
712
713func mapFinishReason(finishReason string) fantasy.FinishReason {
714 switch finishReason {
715 case "end_turn", "pause_turn", "stop_sequence":
716 return fantasy.FinishReasonStop
717 case "max_tokens":
718 return fantasy.FinishReasonLength
719 case "tool_use":
720 return fantasy.FinishReasonToolCalls
721 default:
722 return fantasy.FinishReasonUnknown
723 }
724}
725
726// Generate implements fantasy.LanguageModel.
727func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
728 params, warnings, err := a.prepareParams(call)
729 if err != nil {
730 return nil, err
731 }
732 response, err := a.client.Messages.New(ctx, *params)
733 if err != nil {
734 return nil, a.handleError(err)
735 }
736
737 var content []fantasy.Content
738 for _, block := range response.Content {
739 switch block.Type {
740 case "text":
741 text, ok := block.AsAny().(anthropic.TextBlock)
742 if !ok {
743 continue
744 }
745 content = append(content, fantasy.TextContent{
746 Text: text.Text,
747 })
748 case "thinking":
749 reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
750 if !ok {
751 continue
752 }
753 content = append(content, fantasy.ReasoningContent{
754 Text: reasoning.Thinking,
755 ProviderMetadata: fantasy.ProviderMetadata{
756 Name: &ReasoningOptionMetadata{
757 Signature: reasoning.Signature,
758 },
759 },
760 })
761 case "redacted_thinking":
762 reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
763 if !ok {
764 continue
765 }
766 content = append(content, fantasy.ReasoningContent{
767 Text: "",
768 ProviderMetadata: fantasy.ProviderMetadata{
769 Name: &ReasoningOptionMetadata{
770 RedactedData: reasoning.Data,
771 },
772 },
773 })
774 case "tool_use":
775 toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
776 if !ok {
777 continue
778 }
779 content = append(content, fantasy.ToolCallContent{
780 ToolCallID: toolUse.ID,
781 ToolName: toolUse.Name,
782 Input: string(toolUse.Input),
783 ProviderExecuted: false,
784 })
785 }
786 }
787
788 return &fantasy.Response{
789 Content: content,
790 Usage: fantasy.Usage{
791 InputTokens: response.Usage.InputTokens,
792 OutputTokens: response.Usage.OutputTokens,
793 TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
794 CacheCreationTokens: response.Usage.CacheCreationInputTokens,
795 CacheReadTokens: response.Usage.CacheReadInputTokens,
796 },
797 FinishReason: mapFinishReason(string(response.StopReason)),
798 ProviderMetadata: fantasy.ProviderMetadata{},
799 Warnings: warnings,
800 }, nil
801}
802
803// Stream implements fantasy.LanguageModel.
804func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
805 params, warnings, err := a.prepareParams(call)
806 if err != nil {
807 return nil, err
808 }
809
810 stream := a.client.Messages.NewStreaming(ctx, *params)
811 acc := anthropic.Message{}
812 return func(yield func(fantasy.StreamPart) bool) {
813 if len(warnings) > 0 {
814 if !yield(fantasy.StreamPart{
815 Type: fantasy.StreamPartTypeWarnings,
816 Warnings: warnings,
817 }) {
818 return
819 }
820 }
821
822 for stream.Next() {
823 chunk := stream.Current()
824 _ = acc.Accumulate(chunk)
825 switch chunk.Type {
826 case "content_block_start":
827 contentBlockType := chunk.ContentBlock.Type
828 switch contentBlockType {
829 case "text":
830 if !yield(fantasy.StreamPart{
831 Type: fantasy.StreamPartTypeTextStart,
832 ID: fmt.Sprintf("%d", chunk.Index),
833 }) {
834 return
835 }
836 case "thinking":
837 if !yield(fantasy.StreamPart{
838 Type: fantasy.StreamPartTypeReasoningStart,
839 ID: fmt.Sprintf("%d", chunk.Index),
840 }) {
841 return
842 }
843 case "redacted_thinking":
844 if !yield(fantasy.StreamPart{
845 Type: fantasy.StreamPartTypeReasoningStart,
846 ID: fmt.Sprintf("%d", chunk.Index),
847 ProviderMetadata: fantasy.ProviderMetadata{
848 Name: &ReasoningOptionMetadata{
849 RedactedData: chunk.ContentBlock.Data,
850 },
851 },
852 }) {
853 return
854 }
855 case "tool_use":
856 if !yield(fantasy.StreamPart{
857 Type: fantasy.StreamPartTypeToolInputStart,
858 ID: chunk.ContentBlock.ID,
859 ToolCallName: chunk.ContentBlock.Name,
860 ToolCallInput: "",
861 }) {
862 return
863 }
864 }
865 case "content_block_stop":
866 if len(acc.Content)-1 < int(chunk.Index) {
867 continue
868 }
869 contentBlock := acc.Content[int(chunk.Index)]
870 switch contentBlock.Type {
871 case "text":
872 if !yield(fantasy.StreamPart{
873 Type: fantasy.StreamPartTypeTextEnd,
874 ID: fmt.Sprintf("%d", chunk.Index),
875 }) {
876 return
877 }
878 case "thinking":
879 if !yield(fantasy.StreamPart{
880 Type: fantasy.StreamPartTypeReasoningEnd,
881 ID: fmt.Sprintf("%d", chunk.Index),
882 }) {
883 return
884 }
885 case "tool_use":
886 if !yield(fantasy.StreamPart{
887 Type: fantasy.StreamPartTypeToolInputEnd,
888 ID: contentBlock.ID,
889 }) {
890 return
891 }
892 if !yield(fantasy.StreamPart{
893 Type: fantasy.StreamPartTypeToolCall,
894 ID: contentBlock.ID,
895 ToolCallName: contentBlock.Name,
896 ToolCallInput: string(contentBlock.Input),
897 }) {
898 return
899 }
900 }
901 case "content_block_delta":
902 switch chunk.Delta.Type {
903 case "text_delta":
904 if !yield(fantasy.StreamPart{
905 Type: fantasy.StreamPartTypeTextDelta,
906 ID: fmt.Sprintf("%d", chunk.Index),
907 Delta: chunk.Delta.Text,
908 }) {
909 return
910 }
911 case "thinking_delta":
912 if !yield(fantasy.StreamPart{
913 Type: fantasy.StreamPartTypeReasoningDelta,
914 ID: fmt.Sprintf("%d", chunk.Index),
915 Delta: chunk.Delta.Thinking,
916 }) {
917 return
918 }
919 case "signature_delta":
920 if !yield(fantasy.StreamPart{
921 Type: fantasy.StreamPartTypeReasoningDelta,
922 ID: fmt.Sprintf("%d", chunk.Index),
923 ProviderMetadata: fantasy.ProviderMetadata{
924 Name: &ReasoningOptionMetadata{
925 Signature: chunk.Delta.Signature,
926 },
927 },
928 }) {
929 return
930 }
931 case "input_json_delta":
932 if len(acc.Content)-1 < int(chunk.Index) {
933 continue
934 }
935 contentBlock := acc.Content[int(chunk.Index)]
936 if !yield(fantasy.StreamPart{
937 Type: fantasy.StreamPartTypeToolInputDelta,
938 ID: contentBlock.ID,
939 ToolCallInput: chunk.Delta.PartialJSON,
940 }) {
941 return
942 }
943 }
944 case "message_stop":
945 }
946 }
947
948 err := stream.Err()
949 if err == nil || errors.Is(err, io.EOF) {
950 yield(fantasy.StreamPart{
951 Type: fantasy.StreamPartTypeFinish,
952 ID: acc.ID,
953 FinishReason: mapFinishReason(string(acc.StopReason)),
954 Usage: fantasy.Usage{
955 InputTokens: acc.Usage.InputTokens,
956 OutputTokens: acc.Usage.OutputTokens,
957 TotalTokens: acc.Usage.InputTokens + acc.Usage.OutputTokens,
958 CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
959 CacheReadTokens: acc.Usage.CacheReadInputTokens,
960 },
961 ProviderMetadata: fantasy.ProviderMetadata{},
962 })
963 return
964 } else { //nolint: revive
965 yield(fantasy.StreamPart{
966 Type: fantasy.StreamPartTypeError,
967 Error: a.handleError(err),
968 })
969 return
970 }
971 }, nil
972}