From a587ac145a4a6d5123cf91ab6953762ff3259301 Mon Sep 17 00:00:00 2001 From: Deadwalk Date: Mon, 29 Sep 2025 16:52:19 +0800 Subject: [PATCH] feat(anthropic): support both string and array content formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MessageContent type to handle both string and array content formats - Implement UnmarshalJSON to automatically detect content format - Add ToContentArray() method for unified content processing - Update Message and Response structs to use MessageContent - Fix all content processing logic in main.go and controller - Resolve JSON parsing errors for different content formats 支持Anthropic协议字符串和数组内容格式: - 添加MessageContent类型处理字符串和数组两种内容格式 - 实现UnmarshalJSON自动检测内容格式 - 添加ToContentArray()方法统一内容处理 - 更新Message和Response结构体使用MessageContent - 修复main.go和controller中的所有内容处理逻辑 - 解决不同内容格式的JSON解析错误 --- relay/adaptor/anthropic/main.go | 24 ++++++----- relay/adaptor/anthropic/model.go | 71 +++++++++++++++++++++++++++----- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/relay/adaptor/anthropic/main.go b/relay/adaptor/anthropic/main.go index a52d97d5..3c48d7a6 100644 --- a/relay/adaptor/anthropic/main.go +++ b/relay/adaptor/anthropic/main.go @@ -107,10 +107,12 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { claudeMessage := Message{ Role: message.Role, } - var content Content + var contents []Content if message.IsStringContent() { - content.Type = "text" - content.Text = message.StringContent() + content := Content{ + Type: "text", + Text: message.StringContent(), + } if message.Role == "tool" { claudeMessage.Role = "user" content.Type = "tool_result" @@ -118,21 +120,22 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { content.Text = "" content.ToolUseId = message.ToolCallId } - claudeMessage.Content = append(claudeMessage.Content, content) + contents = append(contents, content) for i := range message.ToolCalls { inputParam := make(map[string]any) _ = json.Unmarshal([]byte(message.ToolCalls[i].Function.Arguments.(string)), &inputParam) - claudeMessage.Content = append(claudeMessage.Content, Content{ + contents = append(contents, Content{ Type: "tool_use", Id: message.ToolCalls[i].Id, Name: message.ToolCalls[i].Function.Name, Input: inputParam, }) } + claudeMessage.Content = MessageContent{value: contents} claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage) continue } - var contents []Content + contents = []Content{} // Reset the slice for reuse openaiContent := message.ParseContent() for _, part := range openaiContent { var content Content @@ -150,7 +153,7 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { } contents = append(contents, content) } - claudeMessage.Content = contents + claudeMessage.Content = MessageContent{value: contents} claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage) } return &claudeRequest @@ -220,11 +223,12 @@ func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo func ResponseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { var responseText string - if len(claudeResponse.Content) > 0 { - responseText = claudeResponse.Content[0].Text + contentArray := claudeResponse.Content.ToContentArray() + if len(contentArray) > 0 { + responseText = contentArray[0].Text } tools := make([]model.Tool, 0) - for _, v := range claudeResponse.Content { + for _, v := range contentArray { if v.Type == "tool_use" { args, _ := json.Marshal(v.Input) tools = append(tools, model.Tool{ diff --git a/relay/adaptor/anthropic/model.go b/relay/adaptor/anthropic/model.go index 3078ecbc..b63f624b 100644 --- a/relay/adaptor/anthropic/model.go +++ b/relay/adaptor/anthropic/model.go @@ -29,9 +29,58 @@ type Content struct { ToolUseId string `json:"tool_use_id,omitempty"` } +// MessageContent can handle both string and array formats for the content field +type MessageContent struct { + value interface{} +} + +// UnmarshalJSON implements json.Unmarshaler to handle both string and array formats +func (m *MessageContent) UnmarshalJSON(data []byte) error { + // Try to unmarshal as string first + var str string + if err := json.Unmarshal(data, &str); err == nil { + m.value = str + return nil + } + + // If that fails, try to unmarshal as array of Content + var arr []Content + if err := json.Unmarshal(data, &arr); err == nil { + m.value = arr + return nil + } + + return fmt.Errorf("message content must be either a string or an array of content blocks") +} + +// MarshalJSON implements json.Marshaler +func (m MessageContent) MarshalJSON() ([]byte, error) { + return json.Marshal(m.value) +} + +// ToContentArray converts the message content to a []Content array +func (m MessageContent) ToContentArray() []Content { + if m.value == nil { + return []Content{} + } + + switch v := m.value.(type) { + case string: + // Convert string to a single text content block + return []Content{{ + Type: "text", + Text: v, + }} + case []Content: + return v + default: + return []Content{} + } +} + type Message struct { - Role string `json:"role"` - Content []Content `json:"content"` + Role string `json:"role"` + Content MessageContent `json:"content"` } type Tool struct { @@ -146,15 +195,15 @@ type Error struct { } type Response struct { - Id string `json:"id"` - Type string `json:"type"` - Role string `json:"role"` - Content []Content `json:"content"` - Model string `json:"model"` - StopReason *string `json:"stop_reason"` - StopSequence *string `json:"stop_sequence"` - Usage Usage `json:"usage"` - Error Error `json:"error"` + Id string `json:"id"` + Type string `json:"type"` + Role string `json:"role"` + Content MessageContent `json:"content"` + Model string `json:"model"` + StopReason *string `json:"stop_reason"` + StopSequence *string `json:"stop_sequence"` + Usage Usage `json:"usage"` + Error Error `json:"error"` } type Delta struct {