mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-21 07:26:47 +08:00
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>
This commit is contained in:
@@ -36,6 +36,12 @@ type MessageContent struct {
|
|||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler to handle both string and array formats
|
// UnmarshalJSON implements json.Unmarshaler to handle both string and array formats
|
||||||
func (m *MessageContent) UnmarshalJSON(data []byte) error {
|
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
|
// Try to unmarshal as string first
|
||||||
var str string
|
var str string
|
||||||
if err := json.Unmarshal(data, &str); err == nil {
|
if err := json.Unmarshal(data, &str); err == nil {
|
||||||
@@ -50,6 +56,14 @@ func (m *MessageContent) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
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")
|
return fmt.Errorf("message content must be either a string or an array of content blocks")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +87,14 @@ func (m MessageContent) ToContentArray() []Content {
|
|||||||
}}
|
}}
|
||||||
case []Content:
|
case []Content:
|
||||||
return v
|
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:
|
default:
|
||||||
return []Content{}
|
return []Content{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
@@ -88,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)
|
||||||
}
|
}
|
||||||
@@ -112,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")
|
||||||
}
|
}
|
||||||
@@ -129,38 +143,24 @@ func getAndValidateAnthropicRequest(c *gin.Context) (*anthropic.Request, error)
|
|||||||
anthropicRequest.MaxTokens = 4096 // default max tokens
|
anthropicRequest.MaxTokens = 4096 // default max tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out messages with empty content to prevent API errors
|
// Add detailed logging for debugging without filtering messages
|
||||||
var validMessages []anthropic.Message
|
logger.Debugf(ctx, "Request details - Model: %s, MaxTokens: %d, Messages: %d",
|
||||||
ctx := c.Request.Context()
|
anthropicRequest.Model, anthropicRequest.MaxTokens, len(anthropicRequest.Messages))
|
||||||
|
|
||||||
for i, message := range anthropicRequest.Messages {
|
for i, message := range anthropicRequest.Messages {
|
||||||
hasContent := false
|
contentArray := message.Content.ToContentArray()
|
||||||
|
logger.Debugf(ctx, "Message[%d] - Role: %s, Content blocks: %d", i, message.Role, len(contentArray))
|
||||||
|
|
||||||
// Check if message has any non-empty content
|
for j, content := range contentArray {
|
||||||
for _, content := range message.Content.ToContentArray() {
|
if content.Type == "tool_use" {
|
||||||
if content.Type == "text" && strings.TrimSpace(content.Text) != "" {
|
logger.Debugf(ctx, " Content[%d] - Type: %s, ID: %s, Name: %s", j, content.Type, content.Id, content.Name)
|
||||||
hasContent = true
|
} else if content.Type == "tool_result" {
|
||||||
break
|
logger.Debugf(ctx, " Content[%d] - Type: %s, ToolUseId: %s, Content length: %d",
|
||||||
} else if content.Type != "text" && content.Type != "" {
|
j, content.Type, content.ToolUseId, len(content.Content))
|
||||||
// Non-text content (like images, tool_use, tool_result) is considered valid
|
} else {
|
||||||
hasContent = true
|
logger.Debugf(ctx, " Content[%d] - Type: %s, Text length: %d", j, content.Type, len(content.Text))
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasContent {
|
|
||||||
validMessages = append(validMessages, message)
|
|
||||||
} else {
|
|
||||||
logger.Warnf(ctx, "Filtered out message at index %d with empty content (role: %s)", i, message.Role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the request with filtered messages
|
|
||||||
anthropicRequest.Messages = validMessages
|
|
||||||
|
|
||||||
// Ensure we still have at least one message after filtering
|
|
||||||
if len(anthropicRequest.Messages) == 0 {
|
|
||||||
return nil, fmt.Errorf("no valid messages found after filtering empty content")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return anthropicRequest, nil
|
return anthropicRequest, nil
|
||||||
@@ -170,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user