Merge pull request #6081 from wucm667/fix/issue-5942-deferred-tools

fix(responses): remove orphan deferred tool flags
This commit is contained in:
Wesley Liddick
2026-08-24 11:23:25 +08:00
committed by GitHub
6 changed files with 109 additions and 0 deletions
@@ -280,6 +280,18 @@ func TestResponsesToChatCompletionsRequest_ToolSearchToolBecomesProxyFunction(t
assert.Contains(t, string(out.Tools[0].Function.Parameters), `"query"`)
}
func TestResponsesToChatCompletionsRequest_DropsDeferredFlagWithToolSearch(t *testing.T) {
var req ResponsesRequest
require.NoError(t, json.Unmarshal([]byte(`{"model":"glm-5.2","input":"hi","tools":[{"type":"tool_search"},{"type":"function","name":"shell","defer_loading":true}]}`), &req))
out, err := ResponsesToChatCompletionsRequest(&req)
require.NoError(t, err)
encoded, err := json.Marshal(out)
require.NoError(t, err)
require.NotContains(t, string(encoded), "defer_loading")
require.Contains(t, string(encoded), `"name":"tool_search"`)
}
// codex 只在 ResponseItem 为 tool_search_call 变体且 execution=client 时执行
// tool search;同名 function_call 会命中 ToolSearchHandler 后因 payload 不匹配
// 触发 FunctionCallError::Fatal,直接中止整个 turn,因此回程必须还原项类型。
@@ -119,6 +119,9 @@ func AdaptResponsesClientTools(req map[string]any) (ResponsesClientToolMapping,
lowered = append(lowered, raw)
}
}
if stripResponsesDeferredToolFlags(lowered) {
changed = true
}
if changed {
req["tools"] = lowered
}
@@ -141,6 +144,26 @@ func AdaptResponsesClientTools(req map[string]any) (ResponsesClientToolMapping,
return adapter, changed, nil
}
// stripResponsesDeferredToolFlags removes defer_loading only when the final
// declaration list no longer contains the built-in tool_search it requires.
func stripResponsesDeferredToolFlags(tools []any) bool {
if hasResponsesToolSearchDeclaration(tools) {
return false
}
changed := false
for _, raw := range tools {
tool, ok := raw.(map[string]any)
if !ok {
continue
}
if _, exists := tool["defer_loading"]; exists {
delete(tool, "defer_loading")
changed = true
}
}
return changed
}
// AdaptResponsesClientToolsWithInheritedMapping lowers client-tool history on
// a follow-up request that omits the session-level tools declaration. An
// explicitly present tools field, including an empty or malformed value,
@@ -66,6 +66,33 @@ func TestAdaptResponsesClientTools_LowersDeclarationsHistoryChoiceAndNamespaces(
require.Equal(t, "team__send", namespaceCall["name"])
}
func TestAdaptResponsesClientTools_RemovesDeferredFlagsWhenToolSearchIsLowered(t *testing.T) {
req := map[string]any{
"tools": []any{
map[string]any{"type": "tool_search"},
map[string]any{"type": "function", "name": "shell", "defer_loading": true},
map[string]any{"type": "function", "name": "apply_patch"},
},
}
_, changed, err := AdaptResponsesClientTools(req)
require.NoError(t, err)
require.True(t, changed)
tools := requireResponsesClientToolValue[[]any](t, req["tools"])
require.Equal(t, toolSearchProxyName, requireResponsesClientToolValue[map[string]any](t, tools[0])["name"])
require.NotContains(t, requireResponsesClientToolValue[map[string]any](t, tools[1]), "defer_loading")
}
func TestStripResponsesDeferredToolFlags_PreservesFlagsWithBuiltInToolSearch(t *testing.T) {
tools := []any{
map[string]any{"type": "tool_search"},
map[string]any{"type": "function", "name": "shell", "defer_loading": true},
}
require.False(t, stripResponsesDeferredToolFlags(tools))
require.Equal(t, true, requireResponsesClientToolValue[map[string]any](t, tools[1])["defer_loading"])
}
func TestAdaptResponsesClientTools_LowersDiscoveredToolSearchOutput(t *testing.T) {
requestJSON := `{
"tools":[{"type":"tool_search"}],
@@ -1091,6 +1091,19 @@ func sanitizeGrokResponsesTools(body []byte) ([]byte, error) {
filteredTools = append(filteredTools, raw)
}
}
if !grokRawToolsContainType(filteredTools, "tool_search") {
for index, raw := range filteredTools {
if !gjson.GetBytes(raw, "defer_loading").Exists() {
continue
}
cleaned, deleteErr := sjson.DeleteBytes(raw, "defer_loading")
if deleteErr != nil {
return nil, deleteErr
}
filteredTools[index] = cleaned
toolsChanged = true
}
}
var err error
if len(filteredTools) != len(rawTools) || toolsChanged {
@@ -1125,6 +1138,15 @@ func sanitizeGrokResponsesTools(body []byte) ([]byte, error) {
return body, nil
}
func grokRawToolsContainType(tools []json.RawMessage, want string) bool {
for _, tool := range tools {
if strings.TrimSpace(gjson.GetBytes(tool, "type").String()) == want {
return true
}
}
return false
}
func deleteGrokOrphanToolControls(body []byte) ([]byte, error) {
var err error
for _, field := range []string{"tool_choice", "parallel_tool_calls"} {
@@ -385,6 +385,16 @@ func TestPatchGrokResponsesBodyDropsToolChoiceWhenNoSupportedToolsRemain(t *test
require.False(t, gjson.GetBytes(patched, "tool_choice").Exists())
}
func TestSanitizeGrokResponsesToolsRemovesDeferredFlagsWithToolSearch(t *testing.T) {
body := []byte(`{"tools":[{"type":"tool_search"},{"type":"function","name":"shell","defer_loading":true},{"type":"function","name":"apply_patch"}]}`)
patched, err := sanitizeGrokResponsesTools(body)
require.NoError(t, err)
require.False(t, gjson.GetBytes(patched, `tools.#(type=="tool_search")`).Exists())
require.False(t, gjson.GetBytes(patched, `tools.#(name=="shell").defer_loading`).Exists())
require.True(t, gjson.GetBytes(patched, `tools.#(name=="apply_patch")`).Exists())
}
func TestSanitizeGrokResponsesToolsKeepsToolChoiceOnlyWithSupportedTools(t *testing.T) {
t.Parallel()
@@ -58,6 +58,21 @@ func TestNormalizeOpenAIResponsesLiteTools_MovesNamespacesAndKeepsSupportedTools
require.Equal(t, map[string]any{"type": "namespace", "name": "collaboration"}, reqBody["tool_choice"])
}
func TestNormalizeOpenAIResponsesLiteTools_PreservesDeferredFlagsWithToolSearch(t *testing.T) {
reqBody := map[string]any{
"tools": []any{
map[string]any{"type": "tool_search"},
map[string]any{"type": "function", "name": "shell", "defer_loading": true},
},
}
_, err := normalizeOpenAIResponsesLiteTools(reqBody)
require.NoError(t, err)
tools := reqBody["tools"].([]any)
require.Equal(t, "tool_search", tools[0].(map[string]any)["type"])
require.Equal(t, true, tools[1].(map[string]any)["defer_loading"])
}
func TestNormalizeOpenAIResponsesLiteTools_RejectsConflictingAdditionalTool(t *testing.T) {
reqBody := map[string]any{
"tools": []any{map[string]any{