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