mirror of
https://github.com/alibaba/higress.git
synced 2026-05-08 04:17:27 +08:00
fix(claude): support array content format in tool_result and remove duplicate structs (#2892)
This commit is contained in:
@@ -69,7 +69,7 @@ type claudeChatMessageContent struct {
|
|||||||
Input map[string]interface{} `json:"input,omitempty"` // For tool_use
|
Input map[string]interface{} `json:"input,omitempty"` // For tool_use
|
||||||
// Tool result fields
|
// Tool result fields
|
||||||
ToolUseId string `json:"tool_use_id,omitempty"` // For tool_result
|
ToolUseId string `json:"tool_use_id,omitempty"` // For tool_result
|
||||||
Content string `json:"content,omitempty"` // For tool_result
|
Content claudeChatMessageContentWr `json:"content,omitempty"` // For tool_result - can be string or array
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON implements custom JSON unmarshaling for claudeChatMessageContentWr
|
// UnmarshalJSON implements custom JSON unmarshaling for claudeChatMessageContentWr
|
||||||
@@ -88,6 +88,8 @@ func (ccw *claudeChatMessageContentWr) UnmarshalJSON(data []byte) error {
|
|||||||
ccw.ArrayValue = arrayValue
|
ccw.ArrayValue = arrayValue
|
||||||
ccw.IsString = false
|
ccw.IsString = false
|
||||||
return nil
|
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")
|
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)
|
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 {
|
func (ccw claudeChatMessageContentWr) GetStringValue() string {
|
||||||
if ccw.IsString {
|
if ccw.IsString {
|
||||||
return ccw.StringValue
|
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
|
// 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 {
|
for _, toolResult := range conversionResult.toolResults {
|
||||||
toolMsg := chatMessage{
|
toolMsg := chatMessage{
|
||||||
Role: "tool",
|
Role: "tool",
|
||||||
Content: toolResult.Content,
|
Content: toolResult.Content.GetStringValue(),
|
||||||
ToolCallId: toolResult.ToolUseId,
|
ToolCallId: toolResult.ToolUseId,
|
||||||
}
|
}
|
||||||
openaiRequest.Messages = append(openaiRequest.Messages, toolMsg)
|
openaiRequest.Messages = append(openaiRequest.Messages, toolMsg)
|
||||||
@@ -271,7 +271,7 @@ func (c *ClaudeToOpenAIConverter) ConvertOpenAIResponseToClaude(ctx wrapper.Http
|
|||||||
var input map[string]interface{}
|
var input map[string]interface{}
|
||||||
if toolCall.Function.Arguments != "" {
|
if toolCall.Function.Arguments != "" {
|
||||||
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &input); err != nil {
|
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{}{}
|
input = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -352,6 +352,79 @@ func TestClaudeToOpenAIConverter_ConvertClaudeRequestToOpenAI(t *testing.T) {
|
|||||||
assert.Equal(t, "toolu_01D7FLrfh4GYq7yT1ULFeyMV", toolMsg.ToolCallId)
|
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) {
|
t.Run("convert_multiple_tool_calls", func(t *testing.T) {
|
||||||
// Test multiple tool_use in single message
|
// Test multiple tool_use in single message
|
||||||
claudeRequest := `{
|
claudeRequest := `{
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
"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/log"
|
||||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
"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/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -81,39 +80,7 @@ func (m *moonshotProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiNam
|
|||||||
if !m.config.isSupportedAPI(apiName) {
|
if !m.config.isSupportedAPI(apiName) {
|
||||||
return types.ActionContinue, errUnsupportedApiName
|
return types.ActionContinue, errUnsupportedApiName
|
||||||
}
|
}
|
||||||
// 非chat类型的请求,不做处理
|
return m.config.handleRequestBody(m, m.contextCache, ctx, apiName, body)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *moonshotProvider) performChatCompletion(ctx wrapper.HttpContext, fileContent string, request *chatCompletionRequest) error {
|
func (m *moonshotProvider) performChatCompletion(ctx wrapper.HttpContext, fileContent string, request *chatCompletionRequest) error {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (m *zhipuAiProviderInitializer) DefaultCapabilities() map[string]string {
|
|||||||
return map[string]string{
|
return map[string]string{
|
||||||
string(ApiNameChatCompletion): zhipuAiChatCompletionPath,
|
string(ApiNameChatCompletion): zhipuAiChatCompletionPath,
|
||||||
string(ApiNameEmbeddings): zhipuAiEmbeddingsPath,
|
string(ApiNameEmbeddings): zhipuAiEmbeddingsPath,
|
||||||
string(ApiNameAnthropicMessages): zhipuAiAnthropicMessagesPath,
|
// string(ApiNameAnthropicMessages): zhipuAiAnthropicMessagesPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user