mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:18:29 +08:00
Merge pull request #6081 from wucm667/fix/issue-5942-deferred-tools
fix(responses): remove orphan deferred tool flags
This commit is contained in:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user