go-llm/anthropic.go
Steve Dudenhoeffer 0d70ec46de Fix role setting for assistant-sent images in Anthropic API
Anthropic API does not support assistants sending images directly, so the role is adjusted to "user" for such messages. This ensures compatibility and prevents errors when processing image messages.
2025-01-09 01:18:11 -05:00

206 lines
4.5 KiB
Go

package go_llm
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"net/http"
anth "github.com/liushuangls/go-anthropic/v2"
)
type anthropic struct {
key string
model string
}
var _ LLM = anthropic{}
func (a anthropic) ModelVersion(modelVersion string) (ChatCompletion, error) {
a.model = modelVersion
// TODO: model verification?
return a, nil
}
func deferClose(c io.Closer) {
err := c.Close()
if err != nil {
slog.Error("error closing", "error", err)
}
}
func (a anthropic) requestToAnthropicRequest(req Request) anth.MessagesRequest {
res := anth.MessagesRequest{
Model: anth.Model(a.model),
MaxTokens: 1000,
}
msgs := []anth.Message{}
// we gotta convert messages into anthropic messages, however
// anthropic does not have a "system" message type, so we need to
// append it to the res.System field instead
for _, msg := range req.Messages {
if msg.Role == RoleSystem {
if len(res.System) > 0 {
res.System += "\n"
}
res.System += msg.Text
} else {
role := anth.RoleUser
if msg.Role == RoleAssistant {
role = anth.RoleAssistant
}
m := anth.Message{
Role: role,
Content: []anth.MessageContent{},
}
if msg.Text != "" {
m.Content = append(m.Content, anth.MessageContent{
Type: anth.MessagesContentTypeText,
Text: &msg.Text,
})
}
for _, img := range msg.Images {
// anthropic doesn't allow the assistant to send images, so we need to say it's from the user
if m.Role == anth.RoleAssistant {
m.Role = anth.RoleUser
}
if img.Base64 != "" {
m.Content = append(m.Content, anth.NewImageMessageContent(
anth.NewMessageContentSource(
anth.MessagesContentSourceTypeBase64,
img.ContentType,
img.Base64,
)))
} else if img.Url != "" {
// download the image
cl, err := http.NewRequest(http.MethodGet, img.Url, nil)
if err != nil {
log.Println("failed to create request", err)
continue
}
resp, err := http.DefaultClient.Do(cl)
if err != nil {
log.Println("failed to download image", err)
continue
}
defer deferClose(resp.Body)
img.ContentType = resp.Header.Get("Content-Type")
// read the image
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("failed to read image", err)
continue
}
// base64 encode the image
img.Base64 = string(b)
m.Content = append(m.Content, anth.NewImageMessageContent(
anth.NewMessageContentSource(
anth.MessagesContentSourceTypeBase64,
img.ContentType,
img.Base64,
)))
}
}
// if this has the same role as the previous message, we can append it to the previous message
// as anthropic expects alternating assistant and user roles
if len(msgs) > 0 && msgs[len(msgs)-1].Role == role {
m2 := &msgs[len(msgs)-1]
m2.Content = append(m2.Content, m.Content...)
} else {
msgs = append(msgs, m)
}
}
}
if req.Toolbox != nil {
for _, tool := range req.Toolbox.funcs {
res.Tools = append(res.Tools, anth.ToolDefinition{
Name: tool.Name,
Description: tool.Description,
InputSchema: tool.Parameters,
})
}
}
res.Messages = msgs
if req.Temperature != nil {
res.Temperature = req.Temperature
}
log.Println("llm request to anthropic request", res)
return res
}
func (a anthropic) responseToLLMResponse(in anth.MessagesResponse) Response {
res := Response{}
for _, msg := range in.Content {
choice := ResponseChoice{}
switch msg.Type {
case anth.MessagesContentTypeText:
if msg.Text != nil {
choice.Content = *msg.Text
}
case anth.MessagesContentTypeToolUse:
if msg.MessageContentToolUse != nil {
b, e := json.Marshal(msg.MessageContentToolUse.Input)
if e != nil {
log.Println("failed to marshal input", e)
} else {
choice.Calls = append(choice.Calls, ToolCall{
ID: msg.MessageContentToolUse.ID,
FunctionCall: FunctionCall{
Name: msg.MessageContentToolUse.Name,
Arguments: string(b),
},
})
}
}
}
res.Choices = append(res.Choices, choice)
}
log.Println("anthropic response to llm response", res)
return res
}
func (a anthropic) ChatComplete(ctx context.Context, req Request) (Response, error) {
cl := anth.NewClient(a.key)
res, err := cl.CreateMessages(ctx, a.requestToAnthropicRequest(req))
if err != nil {
return Response{}, fmt.Errorf("failed to chat complete: %w", err)
}
return a.responseToLLMResponse(res), nil
}