package claude import ( "context" "encoding/base64" "net/http" "net/url" "opencatd-open/internal/model" "opencatd-open/llm" "os" "strings" "github.com/liushuangls/go-anthropic/v2" "github.com/sashabaranov/go-openai" ) type Claude struct { Ctx context.Context ApiKey *model.ApiKey tokenUsage *llm.TokenUsage Done chan struct{} Client *anthropic.Client } func NewClaude(apiKey *model.ApiKey) (*Claude, error) { opts := []anthropic.ClientOption{} if os.Getenv("LOCAL_PROXY") != "" { proxyUrl, err := url.Parse(os.Getenv("LOCAL_PROXY")) if err == nil { client := http.DefaultClient client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} opts = append(opts, anthropic.WithHTTPClient(client)) } } return &Claude{ Ctx: context.Background(), ApiKey: apiKey, tokenUsage: &llm.TokenUsage{}, Done: make(chan struct{}), Client: anthropic.NewClient(*apiKey.ApiKey, opts...), }, nil } func (c *Claude) Chat(ctx context.Context, chatReq llm.ChatRequest) (*llm.ChatResponse, error) { var messages []anthropic.Message if len(chatReq.Messages) > 0 { for _, msg := range chatReq.Messages { var role anthropic.ChatRole if msg.Role != "assistant" { role = anthropic.RoleUser } else { role = anthropic.RoleAssistant } var content []anthropic.MessageContent if len(msg.MultiContent) > 0 { for _, mc := range msg.MultiContent { if mc.Type == "text" { content = append(content, anthropic.MessageContent{Type: anthropic.MessagesContentTypeText, Text: &mc.Text}) } if mc.Type == "image_url" { if strings.HasPrefix(mc.ImageURL.URL, "http") { continue } if strings.HasPrefix(mc.ImageURL.URL, "data:image") { var mediaType string if strings.HasPrefix(mc.ImageURL.URL, "data:image/jpeg") { mediaType = "image/jpeg" } if strings.HasPrefix(mc.ImageURL.URL, "data:image/png") { mediaType = "image/png" } imageString := strings.Split(mc.ImageURL.URL, ",")[1] imageBytes, _ := base64.StdEncoding.DecodeString(imageString) content = append(content, anthropic.MessageContent{Type: "image", Source: &anthropic.MessageContentSource{Type: "base64", MediaType: mediaType, Data: imageBytes}}) } } messages = append(messages, anthropic.Message{Role: role, Content: content}) } } else { if len(msg.Content) > 0 { content = append(content, anthropic.MessageContent{Type: "text", Text: &msg.Content}) } } messages = append(messages, anthropic.Message{Role: role, Content: content}) } } var maxTokens int if chatReq.MaxTokens > 0 { maxTokens = chatReq.MaxTokens } else { if strings.Contains(chatReq.Model, "sonnet") || strings.Contains(chatReq.Model, "haiku") { maxTokens = 8192 } else { maxTokens = 4096 } } resp, err := c.Client.CreateMessages(ctx, anthropic.MessagesRequest{ Model: anthropic.Model(chatReq.Model), Messages: messages, MaxTokens: maxTokens, Stream: false, }) if err != nil { return nil, err } c.tokenUsage.PromptTokens += resp.Usage.InputTokens c.tokenUsage.CompletionTokens += resp.Usage.OutputTokens c.tokenUsage.TotalTokens += resp.Usage.InputTokens + resp.Usage.OutputTokens return &llm.ChatResponse{ Model: string(resp.Model), Choices: []openai.ChatCompletionChoice{ { FinishReason: openai.FinishReason(resp.StopReason), Message: openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, Content: *resp.Content[0].Text, }, }, }, }, nil } func (c *Claude) StreamChat(ctx context.Context, chatReq llm.ChatRequest) (chan *llm.StreamChatResponse, error) { var messages []anthropic.Message if len(chatReq.Messages) > 0 { for _, msg := range chatReq.Messages { var role anthropic.ChatRole if msg.Role != "assistant" { role = anthropic.RoleUser } else { role = anthropic.RoleAssistant } var content []anthropic.MessageContent if len(msg.MultiContent) > 0 { for _, mc := range msg.MultiContent { if mc.Type == "text" { content = append(content, anthropic.MessageContent{Type: anthropic.MessagesContentTypeText, Text: &mc.Text}) } if mc.Type == "image_url" { if strings.HasPrefix(mc.ImageURL.URL, "http") { continue } if strings.HasPrefix(mc.ImageURL.URL, "data:image") { var mediaType string if strings.HasPrefix(mc.ImageURL.URL, "data:image/jpeg") { mediaType = "image/jpeg" } if strings.HasPrefix(mc.ImageURL.URL, "data:image/png") { mediaType = "image/png" } imageString := strings.Split(mc.ImageURL.URL, ",")[1] imageBytes, _ := base64.StdEncoding.DecodeString(imageString) content = append(content, anthropic.MessageContent{Type: "image", Source: &anthropic.MessageContentSource{Type: "base64", MediaType: mediaType, Data: imageBytes}}) } } messages = append(messages, anthropic.Message{Role: role, Content: content}) } } else { if len(msg.Content) > 0 { content = append(content, anthropic.MessageContent{Type: "text", Text: &msg.Content}) } } messages = append(messages, anthropic.Message{Role: role, Content: content}) } } var maxTokens int if chatReq.MaxTokens > 0 { maxTokens = chatReq.MaxTokens } else { if strings.Contains(chatReq.Model, "sonnet") || strings.Contains(chatReq.Model, "haiku") { maxTokens = 8192 } else { maxTokens = 4096 } } datachan := make(chan *llm.StreamChatResponse) // var resp anthropic.MessagesResponse var err error go func() { defer close(datachan) _, err = c.Client.CreateMessagesStream(ctx, anthropic.MessagesStreamRequest{ MessagesRequest: anthropic.MessagesRequest{ Model: anthropic.Model(chatReq.Model), Messages: messages, MaxTokens: maxTokens, }, OnContentBlockDelta: func(data anthropic.MessagesEventContentBlockDeltaData) { datachan <- &llm.StreamChatResponse{ Model: chatReq.Model, Choices: []openai.ChatCompletionStreamChoice{ { Delta: openai.ChatCompletionStreamChoiceDelta{Content: *data.Delta.Text}, }, }, } }, OnMessageStart: func(memss anthropic.MessagesEventMessageStartData) { c.tokenUsage.PromptTokens += memss.Message.Usage.InputTokens c.tokenUsage.CompletionTokens += memss.Message.Usage.OutputTokens c.tokenUsage.TotalTokens += memss.Message.Usage.InputTokens + memss.Message.Usage.OutputTokens }, OnMessageDelta: func(memdd anthropic.MessagesEventMessageDeltaData) { c.tokenUsage.PromptTokens += memdd.Usage.InputTokens c.tokenUsage.CompletionTokens += memdd.Usage.OutputTokens c.tokenUsage.TotalTokens += memdd.Usage.InputTokens + memdd.Usage.OutputTokens datachan <- &llm.StreamChatResponse{ Model: chatReq.Model, Choices: []openai.ChatCompletionStreamChoice{ {FinishReason: openai.FinishReason(memdd.Delta.StopReason)}, }, } }, }) select { case <-ctx.Done(): return default: } }() if err != nil { return nil, err } return datachan, err } func (c *Claude) GetTokenUsage() *llm.TokenUsage { return c.tokenUsage }