Add parallel_tool_calls compatibility mapping

Preserve Chat Completions parallel_tool_calls when converting requests to the Responses API, and map Responses parallel_tool_calls back when falling back to Chat Completions upstreams.

Cover both true and explicit false values so clients can disable parallel tool calls without the field being dropped by omitempty.
This commit is contained in:
CHOS1N
2026-07-09 22:52:35 +08:00
parent e2c731abe4
commit ad8afc8a2e
5 changed files with 48 additions and 6 deletions
@@ -28,6 +28,7 @@ func ResponsesToChatCompletionsRequest(req *ResponsesRequest) (*ChatCompletionsR
TopP: req.TopP,
Stream: req.Stream,
ServiceTier: req.ServiceTier,
ParallelToolCalls: req.ParallelToolCalls,
}
if req.Reasoning != nil {
out.ReasoningEffort = req.Reasoning.Effort
@@ -127,6 +127,26 @@ func TestResponsesToChatCompletionsRequest_TextFormatJsonSchema(t *testing.T) {
}`, string(out.ResponseFormat))
}
func TestResponsesToChatCompletionsRequest_ParallelToolCalls(t *testing.T) {
parallel := false
req := &ResponsesRequest{
Model: "gpt-4o",
Input: json.RawMessage(`[
{"role":"user","content":"Use tools"}
]`),
ParallelToolCalls: &parallel,
}
out, err := ResponsesToChatCompletionsRequest(req)
require.NoError(t, err)
require.NotNil(t, out.ParallelToolCalls)
assert.False(t, *out.ParallelToolCalls)
payload, err := json.Marshal(out)
require.NoError(t, err)
assert.Contains(t, string(payload), `"parallel_tool_calls":false`)
}
func chatMessageRoles(messages []ChatMessage) []string {
roles := make([]string, 0, len(messages))
for _, message := range messages {
@@ -531,6 +531,25 @@ func TestChatCompletionsToResponses_ServiceTier(t *testing.T) {
assert.Equal(t, "flex", resp.ServiceTier)
}
func TestChatCompletionsToResponses_ParallelToolCalls(t *testing.T) {
for _, value := range []bool{false, true} {
req := &ChatCompletionsRequest{
Model: "gpt-4o",
ParallelToolCalls: &value,
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
}
resp, err := ChatCompletionsToResponses(req)
require.NoError(t, err)
require.NotNil(t, resp.ParallelToolCalls)
assert.Equal(t, value, *resp.ParallelToolCalls)
payload, err := json.Marshal(resp)
require.NoError(t, err)
assert.Contains(t, string(payload), `"parallel_tool_calls":`+string(mustMarshalJSON(t, value)))
}
}
// ---------------------------------------------------------------------------
// temperature / top_p stripping for reasoning models
// ---------------------------------------------------------------------------
@@ -27,12 +27,13 @@ func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest,
}
out := &ResponsesRequest{
Model: req.Model,
Instructions: req.Instructions,
Input: inputJSON,
Stream: true, // upstream always streams
Include: []string{"reasoning.encrypted_content"},
ServiceTier: req.ServiceTier,
Model: req.Model,
Instructions: req.Instructions,
Input: inputJSON,
Stream: true, // upstream always streams
Include: []string{"reasoning.encrypted_content"},
ServiceTier: req.ServiceTier,
ParallelToolCalls: req.ParallelToolCalls,
}
// Reasoning models (gpt-5.x) do not accept sampling parameters.
+1
View File
@@ -435,6 +435,7 @@ type ChatCompletionsRequest struct {
Stream bool `json:"stream,omitempty"`
StreamOptions *ChatStreamOptions `json:"stream_options,omitempty"`
Tools []ChatTool `json:"tools,omitempty"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
ReasoningEffort string `json:"reasoning_effort,omitempty"` // "low" | "medium" | "high" | "xhigh"
ServiceTier string `json:"service_tier,omitempty"`