Compare commits

..

3 Commits

Author SHA1 Message Date
Deadwalk
bfadc4e2e0 fix(anthropic): improve MCP tool message handling and error resilience
修复Anthropic协议中MCP工具消息处理问题,增强错误恢复能力:
- 改进MessageContent的JSON解析,支持原始JSON数据保留
- 移除对空内容的过滤,避免丢失MCP工具消息
- 增加详细调试日志,便于问题排查
- 优化错误响应处理,保留响应体信息
- 修复路由转发时可能的数据丢失问题

fix(anthropic): improve MCP tool message handling and error resilience
- Enhance MessageContent JSON parsing to preserve raw JSON data
- Remove empty content filtering to prevent MCP tool message loss
- Add detailed debug logging for troubleshooting
- Optimize error response handling with response body preservation
- Fix potential data loss during routing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 08:29:38 +08:00
Deadwalk
a587ac145a feat(anthropic): support both string and array content formats
- 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解析错误
2025-09-29 16:52:19 +08:00
Deadwalk
7b9631408f fix(anthropic): filter empty content messages to prevent API errors
- Add message content validation in getAndValidateAnthropicRequest
- Filter out messages with empty content to prevent 'all messages must have non-empty content' errors
- Support both text and non-text content validation (images, tool_use, tool_result)
- Add detailed logging for filtered messages
- Ensure at least one valid message remains after filtering

修复Anthropic协议空消息内容问题:
- 在getAndValidateAnthropicRequest中添加消息内容验证
- 过滤空内容消息以防止'all messages must have non-empty content'错误
- 支持文本和非文本内容验证(图片、工具调用、工具结果)
- 为被过滤的消息添加详细日志记录
- 确保过滤后至少保留一条有效消息
2025-09-29 16:51:55 +08:00
3 changed files with 140 additions and 26 deletions

View File

@@ -107,10 +107,12 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request {
claudeMessage := Message{ claudeMessage := Message{
Role: message.Role, Role: message.Role,
} }
var content Content var contents []Content
if message.IsStringContent() { if message.IsStringContent() {
content.Type = "text" content := Content{
content.Text = message.StringContent() Type: "text",
Text: message.StringContent(),
}
if message.Role == "tool" { if message.Role == "tool" {
claudeMessage.Role = "user" claudeMessage.Role = "user"
content.Type = "tool_result" content.Type = "tool_result"
@@ -118,21 +120,22 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request {
content.Text = "" content.Text = ""
content.ToolUseId = message.ToolCallId content.ToolUseId = message.ToolCallId
} }
claudeMessage.Content = append(claudeMessage.Content, content) contents = append(contents, content)
for i := range message.ToolCalls { for i := range message.ToolCalls {
inputParam := make(map[string]any) inputParam := make(map[string]any)
_ = json.Unmarshal([]byte(message.ToolCalls[i].Function.Arguments.(string)), &inputParam) _ = json.Unmarshal([]byte(message.ToolCalls[i].Function.Arguments.(string)), &inputParam)
claudeMessage.Content = append(claudeMessage.Content, Content{ contents = append(contents, Content{
Type: "tool_use", Type: "tool_use",
Id: message.ToolCalls[i].Id, Id: message.ToolCalls[i].Id,
Name: message.ToolCalls[i].Function.Name, Name: message.ToolCalls[i].Function.Name,
Input: inputParam, Input: inputParam,
}) })
} }
claudeMessage.Content = MessageContent{value: contents}
claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage) claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage)
continue continue
} }
var contents []Content contents = []Content{} // Reset the slice for reuse
openaiContent := message.ParseContent() openaiContent := message.ParseContent()
for _, part := range openaiContent { for _, part := range openaiContent {
var content Content var content Content
@@ -150,7 +153,7 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request {
} }
contents = append(contents, content) contents = append(contents, content)
} }
claudeMessage.Content = contents claudeMessage.Content = MessageContent{value: contents}
claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage) claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage)
} }
return &claudeRequest return &claudeRequest
@@ -220,11 +223,12 @@ func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo
func ResponseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { func ResponseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse {
var responseText string var responseText string
if len(claudeResponse.Content) > 0 { contentArray := claudeResponse.Content.ToContentArray()
responseText = claudeResponse.Content[0].Text if len(contentArray) > 0 {
responseText = contentArray[0].Text
} }
tools := make([]model.Tool, 0) tools := make([]model.Tool, 0)
for _, v := range claudeResponse.Content { for _, v := range contentArray {
if v.Type == "tool_use" { if v.Type == "tool_use" {
args, _ := json.Marshal(v.Input) args, _ := json.Marshal(v.Input)
tools = append(tools, model.Tool{ tools = append(tools, model.Tool{

View File

@@ -29,9 +29,80 @@ type Content struct {
ToolUseId string `json:"tool_use_id,omitempty"` 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 {
// Skip empty data or null
if len(data) == 0 || string(data) == "null" {
m.value = ""
return nil
}
// 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
}
// For routing purposes, store raw JSON if we can't parse it
// This ensures we don't lose any data during forwarding
var raw json.RawMessage
if err := json.Unmarshal(data, &raw); err == nil {
m.value = raw
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
case json.RawMessage:
// Try to parse raw JSON as Content array
var arr []Content
if err := json.Unmarshal(v, &arr); err == nil {
return arr
}
// If that fails, return empty array to avoid breaking the routing
return []Content{}
default:
return []Content{}
}
}
type Message struct { type Message struct {
Role string `json:"role"` Role string `json:"role"`
Content []Content `json:"content"` Content MessageContent `json:"content"`
} }
type Tool struct { type Tool struct {
@@ -146,15 +217,15 @@ type Error struct {
} }
type Response struct { type Response struct {
Id string `json:"id"` Id string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Role string `json:"role"` Role string `json:"role"`
Content []Content `json:"content"` Content MessageContent `json:"content"`
Model string `json:"model"` Model string `json:"model"`
StopReason *string `json:"stop_reason"` StopReason *string `json:"stop_reason"`
StopSequence *string `json:"stop_sequence"` StopSequence *string `json:"stop_sequence"`
Usage Usage `json:"usage"` Usage Usage `json:"usage"`
Error Error `json:"error"` Error Error `json:"error"`
} }
type Delta struct { type Delta struct {

View File

@@ -87,7 +87,18 @@ func RelayAnthropicHelper(c *gin.Context) *model.ErrorWithStatusCode {
logger.Debugf(ctx, "Received response - Status: %d", resp.StatusCode) logger.Debugf(ctx, "Received response - Status: %d", resp.StatusCode)
if isErrorHappened(meta, resp) { if isErrorHappened(meta, resp) {
logger.Errorf(ctx, "Error detected in response") logger.Errorf(ctx, "Error detected in response - Status: %d", resp.StatusCode)
// Read response body for detailed error logging
if resp.Body != nil {
bodyBytes, readErr := io.ReadAll(resp.Body)
if readErr == nil {
logger.Errorf(ctx, "Error response body: %s", string(bodyBytes))
// Recreate the body for further processing
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
}
billing.ReturnPreConsumedQuota(ctx, preConsumedQuota, meta.TokenId) billing.ReturnPreConsumedQuota(ctx, preConsumedQuota, meta.TokenId)
return RelayErrorHandler(resp) return RelayErrorHandler(resp)
} }
@@ -111,13 +122,17 @@ func RelayAnthropicHelper(c *gin.Context) *model.ErrorWithStatusCode {
} }
func getAndValidateAnthropicRequest(c *gin.Context) (*anthropic.Request, error) { func getAndValidateAnthropicRequest(c *gin.Context) (*anthropic.Request, error) {
ctx := c.Request.Context()
anthropicRequest := &anthropic.Request{} anthropicRequest := &anthropic.Request{}
err := common.UnmarshalBodyReusable(c, anthropicRequest) err := common.UnmarshalBodyReusable(c, anthropicRequest)
if err != nil { if err != nil {
logger.Errorf(ctx, "Failed to unmarshal Anthropic request: %s", err.Error())
return nil, err return nil, err
} }
// Basic validation logger.Debugf(ctx, "Successfully parsed Anthropic request with %d messages", len(anthropicRequest.Messages))
// Basic validation only - minimal intervention
if anthropicRequest.Model == "" { if anthropicRequest.Model == "" {
return nil, fmt.Errorf("model is required") return nil, fmt.Errorf("model is required")
} }
@@ -128,6 +143,26 @@ func getAndValidateAnthropicRequest(c *gin.Context) (*anthropic.Request, error)
anthropicRequest.MaxTokens = 4096 // default max tokens anthropicRequest.MaxTokens = 4096 // default max tokens
} }
// Add detailed logging for debugging without filtering messages
logger.Debugf(ctx, "Request details - Model: %s, MaxTokens: %d, Messages: %d",
anthropicRequest.Model, anthropicRequest.MaxTokens, len(anthropicRequest.Messages))
for i, message := range anthropicRequest.Messages {
contentArray := message.Content.ToContentArray()
logger.Debugf(ctx, "Message[%d] - Role: %s, Content blocks: %d", i, message.Role, len(contentArray))
for j, content := range contentArray {
if content.Type == "tool_use" {
logger.Debugf(ctx, " Content[%d] - Type: %s, ID: %s, Name: %s", j, content.Type, content.Id, content.Name)
} else if content.Type == "tool_result" {
logger.Debugf(ctx, " Content[%d] - Type: %s, ToolUseId: %s, Content length: %d",
j, content.Type, content.ToolUseId, len(content.Content))
} else {
logger.Debugf(ctx, " Content[%d] - Type: %s, Text length: %d", j, content.Type, len(content.Text))
}
}
}
return anthropicRequest, nil return anthropicRequest, nil
} }
@@ -135,10 +170,14 @@ func getAnthropicRequestBody(c *gin.Context, anthropicRequest *anthropic.Request
// For anthropic native requests, we marshal the request back to JSON // For anthropic native requests, we marshal the request back to JSON
jsonData, err := json.Marshal(anthropicRequest) jsonData, err := json.Marshal(anthropicRequest)
if err != nil { if err != nil {
logger.Debugf(c.Request.Context(), "anthropic request json_marshal_failed: %s\n", err.Error()) logger.Errorf(c.Request.Context(), "Failed to marshal anthropic request: %s", err.Error())
return nil, err return nil, err
} }
logger.Debugf(c.Request.Context(), "anthropic request: \n%s", string(jsonData))
ctx := c.Request.Context()
logger.Debugf(ctx, "Final request body size: %d bytes", len(jsonData))
logger.Debugf(ctx, "Final request body: %s", string(jsonData))
return bytes.NewBuffer(jsonData), nil return bytes.NewBuffer(jsonData), nil
} }
@@ -161,7 +200,7 @@ func estimateAnthropicTokens(request *anthropic.Request) int {
// Count tokens in messages // Count tokens in messages
for _, message := range request.Messages { for _, message := range request.Messages {
for _, content := range message.Content { for _, content := range message.Content.ToContentArray() {
if content.Type == "text" { if content.Type == "text" {
totalTokens += len(content.Text) / CHARS_PER_TOKEN totalTokens += len(content.Text) / CHARS_PER_TOKEN
} }