From b25a6e758a275ab70e8405636e77ffeb7e39831e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E9=82=A6=E6=97=BA?= Date: Sun, 28 Jun 2026 13:04:33 +0800 Subject: [PATCH] fix: filter empty tool names in responses API path Add filterEmptyCoreToolNames() to filter tools with empty names after ToCoreRequest conversion in handleWithAdapters. Root cause: Codex desktop sometimes sends tools with empty names that upstream APIs (DeepSeek, Qwen) reject with a 400 error. The existing filterEmptyToolNames() only worked for chat completions path (ChatTool type). This fix adds a parallel filter for CoreTool type used in the responses API path. --- internal/service/server/adapter_dispatch.go | 4 ++ internal/service/server/filter_empty_tools.go | 21 ++++++ .../service/server/filter_empty_tools_test.go | 69 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 internal/service/server/filter_empty_tools.go create mode 100644 internal/service/server/filter_empty_tools_test.go diff --git a/internal/service/server/adapter_dispatch.go b/internal/service/server/adapter_dispatch.go index d3e65a8d..1f77c770 100644 --- a/internal/service/server/adapter_dispatch.go +++ b/internal/service/server/adapter_dispatch.go @@ -126,6 +126,10 @@ func (s *Server) handleWithAdapters( writeOpenAIError(w, http.StatusInternalServerError, payload) return } + // Filter out tools with empty names — Codex desktop sometimes sends + // tools with empty names that upstream APIs (DeepSeek, Qwen) reject + // with a 400 error. + coreReq.Tools = filterEmptyCoreToolNames(coreReq.Tools) // ------------------------------------------------------------------ // 3. Pick upstream provider candidate, resolve ProviderAdapter. diff --git a/internal/service/server/filter_empty_tools.go b/internal/service/server/filter_empty_tools.go new file mode 100644 index 00000000..a72f76b8 --- /dev/null +++ b/internal/service/server/filter_empty_tools.go @@ -0,0 +1,21 @@ +package server + +import ( + "moonbridge/internal/format" +) + +// filterEmptyCoreToolNames removes tools whose name is empty. +// Codex desktop sometimes sends tools with empty names that +// upstream APIs (DeepSeek, Qwen) reject with a 400 error. +func filterEmptyCoreToolNames(tools []format.CoreTool) []format.CoreTool { + if len(tools) == 0 { + return tools + } + filtered := make([]format.CoreTool, 0, len(tools)) + for _, t := range tools { + if t.Name != "" { + filtered = append(filtered, t) + } + } + return filtered +} diff --git a/internal/service/server/filter_empty_tools_test.go b/internal/service/server/filter_empty_tools_test.go new file mode 100644 index 00000000..1ea3cdbd --- /dev/null +++ b/internal/service/server/filter_empty_tools_test.go @@ -0,0 +1,69 @@ +package server + +import ( + "moonbridge/internal/format" + "testing" +) + +func TestFilterEmptyCoreToolNames(t *testing.T) { + tests := []struct { + name string + tools []format.CoreTool + expected int + }{ + { + name: "empty tools", + tools: []format.CoreTool{}, + expected: 0, + }, + { + name: "no empty names", + tools: []format.CoreTool{ + {Name: "tool1"}, + {Name: "tool2"}, + }, + expected: 2, + }, + { + name: "one empty name at end", + tools: []format.CoreTool{ + {Name: "tool1"}, + {Name: "tool2"}, + {Name: ""}, + }, + expected: 2, + }, + { + name: "one empty name in middle", + tools: []format.CoreTool{ + {Name: "tool1"}, + {Name: ""}, + {Name: "tool3"}, + }, + expected: 2, + }, + { + name: "all empty names", + tools: []format.CoreTool{ + {Name: ""}, + {Name: ""}, + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filterEmptyCoreToolNames(tt.tools) + if len(result) != tt.expected { + t.Errorf("filterEmptyCoreToolNames() returned %d tools, want %d", len(result), tt.expected) + } + // Verify no empty names remain + for _, tool := range result { + if tool.Name == "" { + t.Errorf("filtered result still contains tool with empty name") + } + } + }) + } +}