2025-03-27 22:35:48 +01:00
package provider
import (
"context"
"encoding/json"
"errors"
2025-04-07 10:43:54 -05:00
"fmt"
2025-04-09 20:04:44 +02:00
"io"
2025-03-27 22:35:48 +01:00
"strings"
2025-04-07 10:43:54 -05:00
"time"
2025-03-27 22:35:48 +01:00
"github.com/anthropics/anthropic-sdk-go"
2025-04-09 17:45:41 +02:00
"github.com/anthropics/anthropic-sdk-go/bedrock"
2025-03-27 22:35:48 +01:00
"github.com/anthropics/anthropic-sdk-go/option"
2025-04-24 18:25:52 +02:00
"github.com/opencode-ai/opencode/internal/config"
2025-05-03 01:53:58 +05:30
"github.com/opencode-ai/opencode/internal/llm/models"
2025-04-24 18:25:52 +02:00
"github.com/opencode-ai/opencode/internal/llm/tools"
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/message"
2025-03-27 22:35:48 +01:00
)
2025-04-16 20:06:23 +02:00
type anthropicOptions struct {
useBedrock bool
disableCache bool
shouldThink func ( userMessage string ) bool
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
type AnthropicOption func ( * anthropicOptions )
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
type anthropicClient struct {
providerOptions providerClientOptions
options anthropicOptions
client anthropic . Client
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
type AnthropicClient ProviderClient
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
func newAnthropicClient ( opts providerClientOptions ) AnthropicClient {
anthropicOpts := anthropicOptions { }
for _ , o := range opts . anthropicOptions {
o ( & anthropicOpts )
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
anthropicClientOptions := [ ] option . RequestOption { }
if opts . apiKey != "" {
anthropicClientOptions = append ( anthropicClientOptions , option . WithAPIKey ( opts . apiKey ) )
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
if anthropicOpts . useBedrock {
anthropicClientOptions = append ( anthropicClientOptions , bedrock . WithLoadDefaultConfig ( context . Background ( ) ) )
2025-04-09 17:45:41 +02:00
}
2025-04-16 20:06:23 +02:00
client := anthropic . NewClient ( anthropicClientOptions ... )
return & anthropicClient {
providerOptions : opts ,
options : anthropicOpts ,
client : client ,
2025-04-09 17:45:41 +02:00
}
}
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) convertMessages ( messages [ ] message . Message ) ( anthropicMessages [ ] anthropic . MessageParam ) {
2025-04-20 15:56:36 +02:00
for i , msg := range messages {
cache := false
2025-04-21 07:20:09 +02:00
if i > len ( messages ) - 3 {
2025-04-20 15:56:36 +02:00
cache = true
}
2025-04-16 20:06:23 +02:00
switch msg . Role {
case message . User :
content := anthropic . NewTextBlock ( msg . Content ( ) . String ( ) )
2025-04-20 15:56:36 +02:00
if cache && ! a . options . disableCache {
2025-06-05 10:46:14 -07:00
content . OfText . CacheControl = anthropic . CacheControlEphemeralParam {
2025-04-16 20:06:23 +02:00
Type : "ephemeral" ,
}
}
2025-05-03 01:53:58 +05:30
var contentBlocks [ ] anthropic . ContentBlockParamUnion
contentBlocks = append ( contentBlocks , content )
for _ , binaryContent := range msg . BinaryContent ( ) {
base64Image := binaryContent . String ( models . ProviderAnthropic )
imageBlock := anthropic . NewImageBlockBase64 ( binaryContent . MIMEType , base64Image )
contentBlocks = append ( contentBlocks , imageBlock )
}
anthropicMessages = append ( anthropicMessages , anthropic . NewUserMessage ( contentBlocks ... ) )
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
case message . Assistant :
blocks := [ ] anthropic . ContentBlockParamUnion { }
if msg . Content ( ) . String ( ) != "" {
content := anthropic . NewTextBlock ( msg . Content ( ) . String ( ) )
2025-04-20 15:56:36 +02:00
if cache && ! a . options . disableCache {
2025-06-05 10:46:14 -07:00
content . OfText . CacheControl = anthropic . CacheControlEphemeralParam {
2025-04-16 20:06:23 +02:00
Type : "ephemeral" ,
}
}
blocks = append ( blocks , content )
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
for _ , toolCall := range msg . ToolCalls ( ) {
var inputMap map [ string ] any
err := json . Unmarshal ( [ ] byte ( toolCall . Input ) , & inputMap )
if err != nil {
continue
}
2025-06-05 10:46:14 -07:00
blocks = append ( blocks , anthropic . NewToolUseBlock ( toolCall . ID , inputMap , toolCall . Name ) )
2025-04-16 20:06:23 +02:00
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
if len ( blocks ) == 0 {
2025-04-19 16:35:45 +02:00
logging . Warn ( "There is a message without content, investigate, this should not happen" )
2025-04-16 20:06:23 +02:00
continue
}
anthropicMessages = append ( anthropicMessages , anthropic . NewAssistantMessage ( blocks ... ) )
2025-04-09 17:45:41 +02:00
2025-04-16 20:06:23 +02:00
case message . Tool :
results := make ( [ ] anthropic . ContentBlockParamUnion , len ( msg . ToolResults ( ) ) )
for i , toolResult := range msg . ToolResults ( ) {
results [ i ] = anthropic . NewToolResultBlock ( toolResult . ToolCallID , toolResult . Content , toolResult . IsError )
}
anthropicMessages = append ( anthropicMessages , anthropic . NewUserMessage ( results ... ) )
}
2025-04-09 17:45:41 +02:00
}
2025-04-16 20:06:23 +02:00
return
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) convertTools ( tools [ ] tools . BaseTool ) [ ] anthropic . ToolUnionParam {
anthropicTools := make ( [ ] anthropic . ToolUnionParam , len ( tools ) )
for i , tool := range tools {
info := tool . Info ( )
toolParam := anthropic . ToolParam {
Name : info . Name ,
Description : anthropic . String ( info . Description ) ,
InputSchema : anthropic . ToolInputSchemaParam {
Properties : info . Parameters ,
// TODO: figure out how we can tell claude the required fields?
2025-03-27 22:35:48 +01:00
} ,
2025-04-16 20:06:23 +02:00
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
if i == len ( tools ) - 1 && ! a . options . disableCache {
toolParam . CacheControl = anthropic . CacheControlEphemeralParam {
Type : "ephemeral" ,
}
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
anthropicTools [ i ] = anthropic . ToolUnionParam { OfTool : & toolParam }
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
return anthropicTools
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) finishReason ( reason string ) message . FinishReason {
switch reason {
case "end_turn" :
return message . FinishReasonEndTurn
case "max_tokens" :
return message . FinishReasonMaxTokens
case "tool_use" :
return message . FinishReasonToolUse
case "stop_sequence" :
return message . FinishReasonEndTurn
default :
return message . FinishReasonUnknown
}
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) preparedMessages ( messages [ ] anthropic . MessageParam , tools [ ] anthropic . ToolUnionParam ) anthropic . MessageNewParams {
2025-03-27 22:35:48 +01:00
var thinkingParam anthropic . ThinkingConfigParamUnion
lastMessage := messages [ len ( messages ) - 1 ]
2025-04-16 20:06:23 +02:00
isUser := lastMessage . Role == anthropic . MessageParamRoleUser
messageContent := ""
2025-03-27 22:35:48 +01:00
temperature := anthropic . Float ( 0 )
2025-04-16 20:06:23 +02:00
if isUser {
for _ , m := range lastMessage . Content {
2025-06-05 10:46:14 -07:00
if m . OfText != nil && m . OfText . Text != "" {
messageContent = m . OfText . Text
2025-04-16 20:06:23 +02:00
}
}
if messageContent != "" && a . options . shouldThink != nil && a . options . shouldThink ( messageContent ) {
2025-06-05 10:46:14 -07:00
thinkingParam = anthropic . ThinkingConfigParamOfEnabled ( int64 ( float64 ( a . providerOptions . maxTokens ) * 0.8 ) )
2025-04-16 20:06:23 +02:00
temperature = anthropic . Float ( 1 )
2025-03-27 22:35:48 +01:00
}
}
2025-04-16 20:06:23 +02:00
return anthropic . MessageNewParams {
Model : anthropic . Model ( a . providerOptions . model . APIModel ) ,
MaxTokens : a . providerOptions . maxTokens ,
Temperature : temperature ,
Messages : messages ,
Tools : tools ,
Thinking : thinkingParam ,
System : [ ] anthropic . TextBlockParam {
{
Text : a . providerOptions . systemMessage ,
CacheControl : anthropic . CacheControlEphemeralParam {
Type : "ephemeral" ,
} ,
} ,
} ,
}
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) send ( ctx context . Context , messages [ ] message . Message , tools [ ] tools . BaseTool ) ( resposne * ProviderResponse , err error ) {
preparedMessages := a . preparedMessages ( a . convertMessages ( messages ) , a . convertTools ( tools ) )
cfg := config . Get ( )
if cfg . Debug {
2025-05-03 01:53:58 +05:30
jsonData , _ := json . Marshal ( preparedMessages )
logging . Debug ( "Prepared messages" , "messages" , string ( jsonData ) )
2025-04-16 20:06:23 +02:00
}
2025-05-03 01:53:58 +05:30
2025-04-16 20:06:23 +02:00
attempts := 0
for {
attempts ++
anthropicResponse , err := a . client . Messages . New (
ctx ,
preparedMessages ,
)
// If there is an error we are going to see if we can retry the call
if err != nil {
2025-05-03 01:53:58 +05:30
logging . Error ( "Error in Anthropic API call" , "error" , err )
2025-04-16 20:06:23 +02:00
retry , after , retryErr := a . shouldRetry ( attempts , err )
if retryErr != nil {
return nil , retryErr
}
if retry {
2025-04-25 14:42:21 +02:00
logging . WarnPersist ( fmt . Sprintf ( "Retrying due to rate limit... attempt %d of %d" , attempts , maxRetries ) , logging . PersistTimeArg , time . Millisecond * time . Duration ( after + 100 ) )
2025-04-16 20:06:23 +02:00
select {
case <- ctx . Done ( ) :
return nil , ctx . Err ( )
case <- time . After ( time . Duration ( after ) * time . Millisecond ) :
continue
}
}
return nil , retryErr
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
content := ""
for _ , block := range anthropicResponse . Content {
if text , ok := block . AsAny ( ) . ( anthropic . TextBlock ) ; ok {
content += text . Text
}
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
return & ProviderResponse {
Content : content ,
ToolCalls : a . toolCalls ( * anthropicResponse ) ,
Usage : a . usage ( * anthropicResponse ) ,
} , nil
}
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) stream ( ctx context . Context , messages [ ] message . Message , tools [ ] tools . BaseTool ) <- chan ProviderEvent {
preparedMessages := a . preparedMessages ( a . convertMessages ( messages ) , a . convertTools ( tools ) )
cfg := config . Get ( )
if cfg . Debug {
2025-04-19 16:35:45 +02:00
// jsonData, _ := json.Marshal(preparedMessages)
// logging.Debug("Prepared messages", "messages", string(jsonData))
2025-04-16 20:06:23 +02:00
}
attempts := 0
eventChan := make ( chan ProviderEvent )
go func ( ) {
for {
2025-04-07 10:43:54 -05:00
attempts ++
2025-04-16 20:06:23 +02:00
anthropicStream := a . client . Messages . NewStreaming (
2025-04-07 10:43:54 -05:00
ctx ,
2025-04-16 20:06:23 +02:00
preparedMessages ,
2025-04-07 10:43:54 -05:00
)
accumulatedMessage := anthropic . Message { }
2025-04-19 16:35:45 +02:00
currentToolCallID := ""
2025-04-16 20:06:23 +02:00
for anthropicStream . Next ( ) {
event := anthropicStream . Current ( )
2025-04-07 10:43:54 -05:00
err := accumulatedMessage . Accumulate ( event )
if err != nil {
2025-04-25 21:57:39 +02:00
logging . Warn ( "Error accumulating message" , "error" , err )
2025-04-16 20:06:23 +02:00
continue
2025-04-07 10:43:54 -05:00
}
switch event := event . AsAny ( ) . ( type ) {
case anthropic . ContentBlockStartEvent :
2025-04-19 16:35:45 +02:00
if event . ContentBlock . Type == "text" {
eventChan <- ProviderEvent { Type : EventContentStart }
} else if event . ContentBlock . Type == "tool_use" {
currentToolCallID = event . ContentBlock . ID
eventChan <- ProviderEvent {
Type : EventToolUseStart ,
ToolCall : & message . ToolCall {
ID : event . ContentBlock . ID ,
Name : event . ContentBlock . Name ,
Finished : false ,
} ,
}
}
2025-04-07 10:43:54 -05:00
case anthropic . ContentBlockDeltaEvent :
if event . Delta . Type == "thinking_delta" && event . Delta . Thinking != "" {
eventChan <- ProviderEvent {
Type : EventThinkingDelta ,
Thinking : event . Delta . Thinking ,
}
} else if event . Delta . Type == "text_delta" && event . Delta . Text != "" {
eventChan <- ProviderEvent {
Type : EventContentDelta ,
Content : event . Delta . Text ,
}
2025-04-19 16:35:45 +02:00
} else if event . Delta . Type == "input_json_delta" {
if currentToolCallID != "" {
eventChan <- ProviderEvent {
Type : EventToolUseDelta ,
ToolCall : & message . ToolCall {
ID : currentToolCallID ,
Finished : false ,
Input : event . Delta . JSON . PartialJSON . Raw ( ) ,
} ,
}
}
2025-03-27 22:35:48 +01:00
}
2025-04-07 10:43:54 -05:00
case anthropic . ContentBlockStopEvent :
2025-04-19 16:35:45 +02:00
if currentToolCallID != "" {
eventChan <- ProviderEvent {
Type : EventToolUseStop ,
ToolCall : & message . ToolCall {
ID : currentToolCallID ,
} ,
}
currentToolCallID = ""
} else {
eventChan <- ProviderEvent { Type : EventContentStop }
}
2025-04-07 10:43:54 -05:00
case anthropic . MessageStopEvent :
content := ""
for _ , block := range accumulatedMessage . Content {
if text , ok := block . AsAny ( ) . ( anthropic . TextBlock ) ; ok {
content += text . Text
}
2025-03-27 22:35:48 +01:00
}
2025-04-07 10:43:54 -05:00
eventChan <- ProviderEvent {
Type : EventComplete ,
Response : & ProviderResponse {
Content : content ,
2025-04-16 20:06:23 +02:00
ToolCalls : a . toolCalls ( accumulatedMessage ) ,
Usage : a . usage ( accumulatedMessage ) ,
FinishReason : a . finishReason ( string ( accumulatedMessage . StopReason ) ) ,
2025-04-07 10:43:54 -05:00
} ,
2025-03-27 22:35:48 +01:00
}
}
2025-04-07 10:43:54 -05:00
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
err := anthropicStream . Err ( )
2025-04-09 20:04:44 +02:00
if err == nil || errors . Is ( err , io . EOF ) {
2025-04-16 20:06:23 +02:00
close ( eventChan )
2025-04-07 10:43:54 -05:00
return
}
2025-04-16 20:06:23 +02:00
// If there is an error we are going to see if we can retry the call
retry , after , retryErr := a . shouldRetry ( attempts , err )
if retryErr != nil {
eventChan <- ProviderEvent { Type : EventError , Error : retryErr }
close ( eventChan )
2025-04-08 20:46:40 +02:00
return
}
2025-04-16 20:06:23 +02:00
if retry {
2025-04-25 14:42:21 +02:00
logging . WarnPersist ( fmt . Sprintf ( "Retrying due to rate limit... attempt %d of %d" , attempts , maxRetries ) , logging . PersistTimeArg , time . Millisecond * time . Duration ( after + 100 ) )
2025-04-16 20:06:23 +02:00
select {
case <- ctx . Done ( ) :
// context cancelled
if ctx . Err ( ) != nil {
eventChan <- ProviderEvent { Type : EventError , Error : ctx . Err ( ) }
2025-04-08 11:45:26 -05:00
}
2025-04-16 20:06:23 +02:00
close ( eventChan )
return
case <- time . After ( time . Duration ( after ) * time . Millisecond ) :
continue
2025-03-27 22:35:48 +01:00
}
2025-04-08 20:46:40 +02:00
}
2025-04-16 20:06:23 +02:00
if ctx . Err ( ) != nil {
2025-04-08 20:46:40 +02:00
eventChan <- ProviderEvent { Type : EventError , Error : ctx . Err ( ) }
2025-04-07 10:43:54 -05:00
}
2025-04-08 20:46:40 +02:00
2025-04-16 20:06:23 +02:00
close ( eventChan )
return
2025-03-27 22:35:48 +01:00
}
} ( )
2025-04-16 20:06:23 +02:00
return eventChan
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) shouldRetry ( attempts int , err error ) ( bool , int64 , error ) {
var apierr * anthropic . Error
if ! errors . As ( err , & apierr ) {
return false , 0 , err
}
if apierr . StatusCode != 429 && apierr . StatusCode != 529 {
return false , 0 , err
}
if attempts > maxRetries {
return false , 0 , fmt . Errorf ( "maximum retry attempts reached for rate limit: %d retries" , maxRetries )
}
retryMs := 0
retryAfterValues := apierr . Response . Header . Values ( "Retry-After" )
backoffMs := 2000 * ( 1 << ( attempts - 1 ) )
jitterMs := int ( float64 ( backoffMs ) * 0.2 )
retryMs = backoffMs + jitterMs
if len ( retryAfterValues ) > 0 {
if _ , err := fmt . Sscanf ( retryAfterValues [ 0 ] , "%d" , & retryMs ) ; err == nil {
retryMs = retryMs * 1000
}
}
return true , int64 ( retryMs ) , nil
2025-03-27 22:35:48 +01:00
}
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) toolCalls ( msg anthropic . Message ) [ ] message . ToolCall {
2025-03-27 22:35:48 +01:00
var toolCalls [ ] message . ToolCall
2025-04-16 20:06:23 +02:00
for _ , block := range msg . Content {
2025-03-27 22:35:48 +01:00
switch variant := block . AsAny ( ) . ( type ) {
case anthropic . ToolUseBlock :
toolCall := message . ToolCall {
2025-04-19 16:35:45 +02:00
ID : variant . ID ,
Name : variant . Name ,
Input : string ( variant . Input ) ,
Type : string ( variant . Type ) ,
Finished : true ,
2025-03-27 22:35:48 +01:00
}
toolCalls = append ( toolCalls , toolCall )
}
}
return toolCalls
}
2025-04-16 20:06:23 +02:00
func ( a * anthropicClient ) usage ( msg anthropic . Message ) TokenUsage {
2025-03-27 22:35:48 +01:00
return TokenUsage {
2025-04-16 20:06:23 +02:00
InputTokens : msg . Usage . InputTokens ,
OutputTokens : msg . Usage . OutputTokens ,
CacheCreationTokens : msg . Usage . CacheCreationInputTokens ,
CacheReadTokens : msg . Usage . CacheReadInputTokens ,
2025-03-27 22:35:48 +01:00
}
}
2025-04-16 20:06:23 +02:00
func WithAnthropicBedrock ( useBedrock bool ) AnthropicOption {
return func ( options * anthropicOptions ) {
options . useBedrock = useBedrock
2025-03-27 22:35:48 +01:00
}
}
2025-04-16 20:06:23 +02:00
func WithAnthropicDisableCache ( ) AnthropicOption {
return func ( options * anthropicOptions ) {
options . disableCache = true
}
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
func DefaultShouldThinkFn ( s string ) bool {
return strings . Contains ( strings . ToLower ( s ) , "think" )
}
2025-03-27 22:35:48 +01:00
2025-04-16 20:06:23 +02:00
func WithAnthropicShouldThinkFn ( fn func ( string ) bool ) AnthropicOption {
return func ( options * anthropicOptions ) {
options . shouldThink = fn
2025-03-27 22:35:48 +01:00
}
}