feat: add unit tests for OpenAI responses request conversion

- Introduced a new test file for validating the conversion of OpenAI responses to chat completions.
- Implemented tests to ensure correct merging of consecutive function calls and proper handling of interrupted function calls.
- Enhanced the main conversion function to buffer consecutive function calls and emit them as a single assistant message.
This commit is contained in:
songyu
2026-04-30 13:33:40 +08:00
parent 6ba7c810a7
commit 243c582159
2 changed files with 104 additions and 6 deletions

View File

@@ -57,11 +57,25 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
// Convert input array to messages
if input := root.Get("input"); input.Exists() && input.IsArray() {
pendingToolCalls := make([]interface{}, 0)
flushPendingToolCalls := func() {
if len(pendingToolCalls) == 0 {
return
}
assistantMessage := []byte(`{"role":"assistant","tool_calls":[]}`)
assistantMessage, _ = sjson.SetBytes(assistantMessage, "tool_calls", pendingToolCalls)
out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage)
pendingToolCalls = pendingToolCalls[:0]
}
input.ForEach(func(_, item gjson.Result) bool {
itemType := item.Get("type").String()
if itemType == "" && item.Get("role").String() != "" {
itemType = "message"
}
if itemType != "function_call" {
flushPendingToolCalls()
}
switch itemType {
case "message", "":
@@ -112,9 +126,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
out, _ = sjson.SetRawBytes(out, "messages.-1", message)
case "function_call":
// Handle function call conversion to assistant message with tool_calls
assistantMessage := []byte(`{"role":"assistant","tool_calls":[]}`)
// Buffer consecutive function calls and emit them as one assistant message.
toolCall := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
if callId := item.Get("call_id"); callId.Exists() {
@@ -128,9 +140,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
if arguments := item.Get("arguments"); arguments.Exists() {
toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", arguments.String())
}
assistantMessage, _ = sjson.SetRawBytes(assistantMessage, "tool_calls.0", toolCall)
out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage)
pendingToolCalls = append(pendingToolCalls, gjson.ParseBytes(toolCall).Value())
case "function_call_output":
// Handle function call output conversion to tool message
@@ -149,6 +159,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
return true
})
flushPendingToolCalls()
} else if input.Type == gjson.String {
msg := []byte(`{}`)
msg, _ = sjson.SetBytes(msg, "role", "user")

View File

@@ -0,0 +1,87 @@
package responses
import (
"bytes"
"encoding/json"
"testing"
"github.com/tidwall/gjson"
)
func prettyJSONForTest(raw []byte) string {
if !gjson.ValidBytes(raw) {
return string(raw)
}
var out bytes.Buffer
if err := json.Indent(&out, raw, "", " "); err != nil {
return string(raw)
}
return out.String()
}
func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_MergeConsecutiveFunctionCalls(t *testing.T) {
raw := []byte(`{
"input": [
{"type":"function_call","call_id":"exec_command:0","name":"exec_command","arguments":"{\"cmd\":\"ls\"}"},
{"type":"function_call","call_id":"exec_command:1","name":"exec_command","arguments":"{\"cmd\":\"pwd\"}"},
{"type":"function_call_output","call_id":"exec_command:0","output":"ok0"},
{"type":"function_call_output","call_id":"exec_command:1","output":"ok1"}
]
}`)
t.Logf("input json:\n%s", prettyJSONForTest(raw))
out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("kimi-k2.6", raw, true)
t.Logf("output json:\n%s", prettyJSONForTest(out))
msgs := gjson.GetBytes(out, "messages")
if !msgs.Exists() || !msgs.IsArray() {
t.Fatalf("messages should be an array")
}
if got := len(msgs.Array()); got != 3 {
t.Fatalf("messages count = %d, want %d", got, 3)
}
if got := gjson.GetBytes(out, "messages.0.role").String(); got != "assistant" {
t.Fatalf("messages.0.role = %q, want %q", got, "assistant")
}
if got := len(gjson.GetBytes(out, "messages.0.tool_calls").Array()); got != 2 {
t.Fatalf("messages.0.tool_calls length = %d, want %d", got, 2)
}
if got := gjson.GetBytes(out, "messages.0.tool_calls.0.id").String(); got != "exec_command:0" {
t.Fatalf("messages.0.tool_calls.0.id = %q, want %q", got, "exec_command:0")
}
if got := gjson.GetBytes(out, "messages.0.tool_calls.1.id").String(); got != "exec_command:1" {
t.Fatalf("messages.0.tool_calls.1.id = %q, want %q", got, "exec_command:1")
}
if got := gjson.GetBytes(out, "messages.1.tool_call_id").String(); got != "exec_command:0" {
t.Fatalf("messages.1.tool_call_id = %q, want %q", got, "exec_command:0")
}
if got := gjson.GetBytes(out, "messages.2.tool_call_id").String(); got != "exec_command:1" {
t.Fatalf("messages.2.tool_call_id = %q, want %q", got, "exec_command:1")
}
}
func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_SplitFunctionCallsWhenInterrupted(t *testing.T) {
raw := []byte(`{
"input": [
{"type":"function_call","call_id":"call_a","name":"tool_a","arguments":"{}"},
{"type":"message","role":"user","content":"next"},
{"type":"function_call","call_id":"call_b","name":"tool_b","arguments":"{}"}
]
}`)
t.Logf("input json:\n%s", prettyJSONForTest(raw))
out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("kimi-k2.6", raw, false)
t.Logf("output json:\n%s", prettyJSONForTest(out))
if got := len(gjson.GetBytes(out, "messages").Array()); got != 3 {
t.Fatalf("messages count = %d, want %d", got, 3)
}
if got := gjson.GetBytes(out, "messages.0.tool_calls.0.id").String(); got != "call_a" {
t.Fatalf("messages.0.tool_calls.0.id = %q, want %q", got, "call_a")
}
if got := gjson.GetBytes(out, "messages.2.tool_calls.0.id").String(); got != "call_b" {
t.Fatalf("messages.2.tool_calls.0.id = %q, want %q", got, "call_b")
}
}