From 75fa62653f22cde7777b50a13cb69520962d3b8d Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sat, 20 Jun 2026 09:47:45 +0800 Subject: [PATCH] feat(executor): normalize `parallel_tool_calls` based on `tools` presence - Added `normalizeCodexParallelToolCallsForTools` to conditionally remove `parallel_tool_calls` when `tools` are missing or empty. - Integrated normalization into Codex executor workflows for improved request handling. - Introduced unit tests to validate behavior across different tool scenarios. Closes: #3903 --- internal/runtime/executor/codex_executor.go | 18 +++++++++ ...codex_executor_parallel_tool_calls_test.go | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 internal/runtime/executor/codex_executor_parallel_tool_calls_test.go diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 24a520cc..7b69f67d 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -829,6 +829,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re body = ensureImageGenerationTool(body, baseModel, auth) } body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body) + body = normalizeCodexParallelToolCallsForTools(body) body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body) if errReplay != nil { return resp, errReplay @@ -1004,6 +1005,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A body = ensureImageGenerationTool(body, baseModel, auth) } body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body) + body = normalizeCodexParallelToolCallsForTools(body) reporter.SetTranslatedReasoningEffort(body, to.String()) url := strings.TrimSuffix(baseURL, "/") + "/responses/compact" @@ -1113,6 +1115,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au body = ensureImageGenerationTool(body, baseModel, auth) } body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body) + body = normalizeCodexParallelToolCallsForTools(body) body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body) if errReplay != nil { return nil, errReplay @@ -1775,6 +1778,21 @@ func ensureImageGenerationTool(body []byte, baseModel string, auth *cliproxyauth return body } +func normalizeCodexParallelToolCallsForTools(body []byte) []byte { + if !gjson.GetBytes(body, "parallel_tool_calls").Exists() { + return body + } + + tools := gjson.GetBytes(body, "tools") + hasTools := tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 + if hasTools { + return body + } + + body, _ = sjson.DeleteBytes(body, "parallel_tool_calls") + return body +} + func publishCodexImageToolUsage(ctx context.Context, reporter *helps.UsageReporter, body []byte, completedData []byte) { detail, ok := helps.ParseCodexImageToolUsage(completedData) if !ok { diff --git a/internal/runtime/executor/codex_executor_parallel_tool_calls_test.go b/internal/runtime/executor/codex_executor_parallel_tool_calls_test.go new file mode 100644 index 00000000..d1f4f8e1 --- /dev/null +++ b/internal/runtime/executor/codex_executor_parallel_tool_calls_test.go @@ -0,0 +1,40 @@ +package executor + +import ( + "testing" + + "github.com/tidwall/gjson" +) + +func TestNormalizeCodexParallelToolCallsForTools_DropsWhenToolsMissing(t *testing.T) { + body := []byte(`{"model":"gpt-5.4","parallel_tool_calls":true,"input":"hi"}`) + + out := normalizeCodexParallelToolCallsForTools(body) + + if gjson.GetBytes(out, "parallel_tool_calls").Exists() { + t.Fatalf("parallel_tool_calls should be removed when tools are missing: %s", string(out)) + } +} + +func TestNormalizeCodexParallelToolCallsForTools_DropsWhenToolsEmpty(t *testing.T) { + body := []byte(`{"model":"gpt-5.4","tools":[],"parallel_tool_calls":false,"input":"hi"}`) + + out := normalizeCodexParallelToolCallsForTools(body) + + if gjson.GetBytes(out, "parallel_tool_calls").Exists() { + t.Fatalf("parallel_tool_calls should be removed when tools are empty: %s", string(out)) + } + if !gjson.GetBytes(out, "tools").Exists() { + t.Fatalf("tools should be preserved: %s", string(out)) + } +} + +func TestNormalizeCodexParallelToolCallsForTools_PreservesWhenToolsPresent(t *testing.T) { + body := []byte(`{"model":"gpt-5.4","tools":[{"type":"function","name":"lookup"}],"parallel_tool_calls":true,"input":"hi"}`) + + out := normalizeCodexParallelToolCallsForTools(body) + + if !gjson.GetBytes(out, "parallel_tool_calls").Bool() { + t.Fatalf("parallel_tool_calls should be preserved when tools are present: %s", string(out)) + } +} -- 2.51.2