diff --git a/dto/openai_response.go b/dto/openai_response.go index 8d727dab108..5b2172eea20 100644 --- a/dto/openai_response.go +++ b/dto/openai_response.go @@ -134,8 +134,37 @@ type FunctionResponse struct { Description string `json:"description,omitempty"` Name string `json:"name,omitempty"` // call function with arguments in JSON format - Parameters any `json:"parameters,omitempty"` // request - Arguments string `json:"arguments"` // response + Parameters any `json:"parameters,omitempty"` // request + Arguments ResponseArguments `json:"arguments"` // response +} + +// ResponseArguments accepts both the canonical JSON string form and the object +// form occasionally emitted by Responses streaming events. +type ResponseArguments string + +func (a *ResponseArguments) UnmarshalJSON(data []byte) error { + if len(data) == 0 || string(data) == "null" { + *a = "" + return nil + } + var s string + if err := json.Unmarshal(data, &s); err == nil { + *a = ResponseArguments(s) + return nil + } + if !json.Valid(data) { + return fmt.Errorf("invalid response arguments JSON") + } + *a = ResponseArguments(string(data)) + return nil +} + +func (a ResponseArguments) MarshalJSON() ([]byte, error) { + return json.Marshal(string(a)) +} + +func (a ResponseArguments) String() string { + return string(a) } type ChatCompletionsStreamResponse struct { @@ -346,7 +375,7 @@ type ResponsesOutput struct { Size string `json:"size"` CallId string `json:"call_id,omitempty"` Name string `json:"name,omitempty"` - Arguments string `json:"arguments,omitempty"` + Arguments ResponseArguments `json:"arguments,omitempty"` } type ResponsesOutputContent struct { diff --git a/dto/openai_response_test.go b/dto/openai_response_test.go new file mode 100644 index 00000000000..28dde2b5da6 --- /dev/null +++ b/dto/openai_response_test.go @@ -0,0 +1,50 @@ +package dto + +import ( + "encoding/json" + "testing" +) + +func TestResponsesOutputArgumentsAcceptsObject(t *testing.T) { + var output ResponsesOutput + err := json.Unmarshal([]byte(`{"type":"function_call","arguments":{"query":"hello","limit":3}}`), &output) + if err != nil { + t.Fatalf("Unmarshal ResponsesOutput failed: %v", err) + } + if got := output.Arguments.String(); got != `{"query":"hello","limit":3}` { + t.Fatalf("unexpected arguments: %s", got) + } +} + +func TestResponsesOutputArgumentsAcceptsNull(t *testing.T) { + var output ResponsesOutput + err := json.Unmarshal([]byte(`{"type":"function_call","arguments":null}`), &output) + if err != nil { + t.Fatalf("Unmarshal ResponsesOutput failed: %v", err) + } + if got := output.Arguments.String(); got != "" { + t.Fatalf("unexpected arguments: %s", got) + } +} + +func TestResponsesOutputArgumentsAcceptsArray(t *testing.T) { + var output ResponsesOutput + err := json.Unmarshal([]byte(`{"type":"function_call","arguments":["hello",3]}`), &output) + if err != nil { + t.Fatalf("Unmarshal ResponsesOutput failed: %v", err) + } + if got := output.Arguments.String(); got != `["hello",3]` { + t.Fatalf("unexpected arguments: %s", got) + } +} + +func TestFunctionResponseArgumentsAcceptsString(t *testing.T) { + var fn FunctionResponse + err := json.Unmarshal([]byte(`{"name":"x","arguments":"{\"query\":\"hello\"}"}`), &fn) + if err != nil { + t.Fatalf("Unmarshal FunctionResponse failed: %v", err) + } + if got := fn.Arguments.String(); got != `{"query":"hello"}` { + t.Fatalf("unexpected arguments: %s", got) + } +} diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index fa8234523c7..661b5f90a4c 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -485,7 +485,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo Type: "function", Index: common.GetPointer(fcIdx), Function: dto.FunctionResponse{ - Arguments: *claudeResponse.Delta.PartialJson, + Arguments: dto.ResponseArguments(*claudeResponse.Delta.PartialJson), }, }) case "signature_delta": @@ -546,7 +546,7 @@ func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextRe Type: "function", // compatible with other OpenAI derivative applications Function: dto.FunctionResponse{ Name: message.Name, - Arguments: string(args), + Arguments: dto.ResponseArguments(string(args)), }, }) case "thinking": diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 21641e48386..ad522f3d315 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -1005,7 +1005,7 @@ func getResponseToolCall(item *dto.GeminiPart) *dto.ToolCallResponse { ID: fmt.Sprintf("call_%s", common.GetUUID()), Type: "function", Function: dto.FunctionResponse{ - Arguments: string(argsBytes), + Arguments: dto.ResponseArguments(string(argsBytes)), Name: item.FunctionCall.FunctionName, }, } diff --git a/relay/channel/ollama/stream.go b/relay/channel/ollama/stream.go index 2a264b27e46..8962d00a665 100644 --- a/relay/channel/ollama/stream.go +++ b/relay/channel/ollama/stream.go @@ -138,7 +138,7 @@ func ollamaStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http // arguments -> string argBytes, _ := json.Marshal(tc.Function.Arguments) toolId := fmt.Sprintf("call_%d", toolCallIndex) - tr := dto.ToolCallResponse{ID: toolId, Type: "function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: string(argBytes)}} + tr := dto.ToolCallResponse{ID: toolId, Type: "function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: dto.ResponseArguments(string(argBytes))}} tr.SetIndex(toolCallIndex) toolCallIndex++ delta.Choices[0].Delta.ToolCalls = append(delta.Choices[0].Delta.ToolCalls, tr) diff --git a/relay/channel/openai/chat_via_responses.go b/relay/channel/openai/chat_via_responses.go index 5e8ec173de1..4fa6c111a67 100644 --- a/relay/channel/openai/chat_via_responses.go +++ b/relay/channel/openai/chat_via_responses.go @@ -258,7 +258,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo ID: callID, Type: "function", Function: dto.FunctionResponse{ - Arguments: argsDelta, + Arguments: dto.ResponseArguments(argsDelta), }, } tool.SetIndex(idx) @@ -408,7 +408,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo toolCallNameByID[callID] = name } - newArgs := streamResp.Item.Arguments + newArgs := streamResp.Item.Arguments.String() prevArgs := toolCallArgsByID[callID] argsDelta := "" if newArgs != "" { diff --git a/relay/channel/openai/helper.go b/relay/channel/openai/helper.go index 08811a77205..6e01b026745 100644 --- a/relay/channel/openai/helper.go +++ b/relay/channel/openai/helper.go @@ -85,7 +85,7 @@ func ProcessStreamResponse(streamResponse dto.ChatCompletionsStreamResponse, res } for _, tool := range choice.Delta.ToolCalls { responseTextBuilder.WriteString(tool.Function.Name) - responseTextBuilder.WriteString(tool.Function.Arguments) + responseTextBuilder.WriteString(tool.Function.Arguments.String()) } } } @@ -132,7 +132,7 @@ func processChatCompletions(streamResp string, streamItems []string, responseTex } for _, tool := range choice.Delta.ToolCalls { responseTextBuilder.WriteString(tool.Function.Name) - responseTextBuilder.WriteString(tool.Function.Arguments) + responseTextBuilder.WriteString(tool.Function.Arguments.String()) } } } diff --git a/service/convert.go b/service/convert.go index 95acf835ee4..11c4f9f922f 100644 --- a/service/convert.go +++ b/service/convert.go @@ -343,13 +343,14 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon claudeResponses = append(claudeResponses, resp) // 首块包含工具 delta,则追加 input_json_delta if toolCall.Function.Arguments != "" { + partialJSON := toolCall.Function.Arguments.String() idx := 0 claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Index: &idx, Type: "content_block_delta", Delta: &dto.ClaudeMediaMessage{ Type: "input_json_delta", - PartialJson: &toolCall.Function.Arguments, + PartialJson: &partialJSON, }, }) } @@ -515,12 +516,13 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon } if len(toolCall.Function.Arguments) > 0 { + partialJSON := toolCall.Function.Arguments.String() claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Index: &idx, Type: "content_block_delta", Delta: &dto.ClaudeMediaMessage{ Type: "input_json_delta", - PartialJson: &toolCall.Function.Arguments, + PartialJson: &partialJSON, }, }) }