mirror of
https://github.com/alibaba/higress.git
synced 2026-02-27 06:00:51 +08:00
fix(claude): support array content format in tool_result and remove duplicate structs (#2892)
This commit is contained in:
@@ -68,8 +68,8 @@ type claudeChatMessageContent struct {
|
||||
Name string `json:"name,omitempty"` // For tool_use
|
||||
Input map[string]interface{} `json:"input,omitempty"` // For tool_use
|
||||
// Tool result fields
|
||||
ToolUseId string `json:"tool_use_id,omitempty"` // For tool_result
|
||||
Content string `json:"content,omitempty"` // For tool_result
|
||||
ToolUseId string `json:"tool_use_id,omitempty"` // For tool_result
|
||||
Content claudeChatMessageContentWr `json:"content,omitempty"` // For tool_result - can be string or array
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshaling for claudeChatMessageContentWr
|
||||
@@ -88,6 +88,8 @@ func (ccw *claudeChatMessageContentWr) UnmarshalJSON(data []byte) error {
|
||||
ccw.ArrayValue = arrayValue
|
||||
ccw.IsString = false
|
||||
return nil
|
||||
} else {
|
||||
log.Errorf("claude chat message unmarshal failed, data:%s, err:%v", data, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("content field must be either a string or an array of content blocks")
|
||||
@@ -101,12 +103,19 @@ func (ccw claudeChatMessageContentWr) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(ccw.ArrayValue)
|
||||
}
|
||||
|
||||
// GetStringValue returns the string representation if it's a string, empty string otherwise
|
||||
// GetStringValue returns the string representation if it's a string, or concatenated text from array blocks
|
||||
func (ccw claudeChatMessageContentWr) GetStringValue() string {
|
||||
if ccw.IsString {
|
||||
return ccw.StringValue
|
||||
}
|
||||
return ""
|
||||
// If it's an array, concatenate text content from all blocks
|
||||
var parts []string
|
||||
for _, block := range ccw.ArrayValue {
|
||||
if block.Text != "" {
|
||||
parts = append(parts, block.Text)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// GetArrayValue returns the array representation if it's an array, empty slice otherwise
|
||||
|
||||
@@ -108,7 +108,7 @@ func (c *ClaudeToOpenAIConverter) ConvertClaudeRequestToOpenAI(body []byte) ([]b
|
||||
for _, toolResult := range conversionResult.toolResults {
|
||||
toolMsg := chatMessage{
|
||||
Role: "tool",
|
||||
Content: toolResult.Content,
|
||||
Content: toolResult.Content.GetStringValue(),
|
||||
ToolCallId: toolResult.ToolUseId,
|
||||
}
|
||||
openaiRequest.Messages = append(openaiRequest.Messages, toolMsg)
|
||||
@@ -271,7 +271,7 @@ func (c *ClaudeToOpenAIConverter) ConvertOpenAIResponseToClaude(ctx wrapper.Http
|
||||
var input map[string]interface{}
|
||||
if toolCall.Function.Arguments != "" {
|
||||
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &input); err != nil {
|
||||
log.Errorf("Failed to parse tool call arguments: %v", err)
|
||||
log.Errorf("Failed to parse tool call arguments: %v, arguments: %s", err, toolCall.Function.Arguments)
|
||||
input = map[string]interface{}{}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -352,6 +352,79 @@ func TestClaudeToOpenAIConverter_ConvertClaudeRequestToOpenAI(t *testing.T) {
|
||||
assert.Equal(t, "toolu_01D7FLrfh4GYq7yT1ULFeyMV", toolMsg.ToolCallId)
|
||||
})
|
||||
|
||||
t.Run("convert_tool_result_with_array_content", func(t *testing.T) {
|
||||
// Test Claude tool_result with array content format (new format that was causing the error)
|
||||
claudeRequest := `{
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_vrtx_01UbCfwoTgoDBqbYEwkVaxd5",
|
||||
"content": [{
|
||||
"text": "Search results for three.js libraries and frameworks",
|
||||
"type": "text"
|
||||
}]
|
||||
}]
|
||||
}],
|
||||
"max_tokens": 1000
|
||||
}`
|
||||
|
||||
result, err := converter.ConvertClaudeRequestToOpenAI([]byte(claudeRequest))
|
||||
require.NoError(t, err)
|
||||
|
||||
var openaiRequest chatCompletionRequest
|
||||
err = json.Unmarshal(result, &openaiRequest)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have one tool message
|
||||
require.Len(t, openaiRequest.Messages, 1)
|
||||
toolMsg := openaiRequest.Messages[0]
|
||||
|
||||
assert.Equal(t, "tool", toolMsg.Role)
|
||||
assert.Equal(t, "Search results for three.js libraries and frameworks", toolMsg.Content)
|
||||
assert.Equal(t, "toolu_vrtx_01UbCfwoTgoDBqbYEwkVaxd5", toolMsg.ToolCallId)
|
||||
})
|
||||
|
||||
t.Run("convert_tool_result_with_actual_error_data", func(t *testing.T) {
|
||||
// Test using the actual JSON data from the error log to ensure our fix works
|
||||
claudeRequest := `{
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"content": [{
|
||||
"text": "\n ## 结果 1\n - **id**: /websites/threejs\n - **title**: three.js\n - **description**: three.js is a JavaScript 3D library that makes it easy to create and display animated 3D computer graphics in a web browser. It provides a powerful and flexible way to build interactive 3D experiences.\n",
|
||||
"type": "text"
|
||||
}],
|
||||
"tool_use_id": "toolu_vrtx_01UbCfwoTgoDBqbYEwkVaxd5",
|
||||
"type": "tool_result"
|
||||
}, {
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
"text": "继续",
|
||||
"type": "text"
|
||||
}]
|
||||
}],
|
||||
"max_tokens": 1000
|
||||
}`
|
||||
|
||||
result, err := converter.ConvertClaudeRequestToOpenAI([]byte(claudeRequest))
|
||||
require.NoError(t, err)
|
||||
|
||||
var openaiRequest chatCompletionRequest
|
||||
err = json.Unmarshal(result, &openaiRequest)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have one tool message (the text content is included in the same message array)
|
||||
require.Len(t, openaiRequest.Messages, 1)
|
||||
|
||||
// Should be tool message
|
||||
toolMsg := openaiRequest.Messages[0]
|
||||
assert.Equal(t, "tool", toolMsg.Role)
|
||||
assert.Contains(t, toolMsg.Content, "three.js")
|
||||
assert.Equal(t, "toolu_vrtx_01UbCfwoTgoDBqbYEwkVaxd5", toolMsg.ToolCallId)
|
||||
})
|
||||
|
||||
t.Run("convert_multiple_tool_calls", func(t *testing.T) {
|
||||
// Test multiple tool_use in single message
|
||||
claudeRequest := `{
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
@@ -81,39 +80,7 @@ func (m *moonshotProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiNam
|
||||
if !m.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
// 非chat类型的请求,不做处理
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
request := &chatCompletionRequest{}
|
||||
if err := m.config.parseRequestAndMapModel(ctx, request, body); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
|
||||
if m.config.moonshotFileId == "" && m.contextCache == nil {
|
||||
return types.ActionContinue, replaceJsonRequestBody(request)
|
||||
}
|
||||
|
||||
apiKey := m.config.GetOrSetTokenWithContext(ctx)
|
||||
err := m.getContextContent(apiKey, func(content string, err error) {
|
||||
defer func() {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
}()
|
||||
if err != nil {
|
||||
log.Errorf("failed to load context file: %v", err)
|
||||
_ = util.ErrorHandler("ai-proxy.moonshot.load_ctx_failed", fmt.Errorf("failed to load context file: %v", err))
|
||||
return
|
||||
}
|
||||
err = m.performChatCompletion(ctx, content, request)
|
||||
if err != nil {
|
||||
_ = util.ErrorHandler("ai-proxy.moonshot.insert_ctx_failed", fmt.Errorf("failed to perform chat completion: %v", err))
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
return types.ActionPause, nil
|
||||
}
|
||||
return types.ActionContinue, err
|
||||
return m.config.handleRequestBody(m, m.contextCache, ctx, apiName, body)
|
||||
}
|
||||
|
||||
func (m *moonshotProvider) performChatCompletion(ctx wrapper.HttpContext, fileContent string, request *chatCompletionRequest) error {
|
||||
|
||||
@@ -28,9 +28,9 @@ func (m *zhipuAiProviderInitializer) ValidateConfig(config *ProviderConfig) erro
|
||||
|
||||
func (m *zhipuAiProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): zhipuAiChatCompletionPath,
|
||||
string(ApiNameEmbeddings): zhipuAiEmbeddingsPath,
|
||||
string(ApiNameAnthropicMessages): zhipuAiAnthropicMessagesPath,
|
||||
string(ApiNameChatCompletion): zhipuAiChatCompletionPath,
|
||||
string(ApiNameEmbeddings): zhipuAiEmbeddingsPath,
|
||||
// string(ApiNameAnthropicMessages): zhipuAiAnthropicMessagesPath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user