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