mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(apicompat): 强制选择 tool_search 的 tool_choice 降级为指向代理的 function 选择
tool_search 工具未被丢弃而是降级为同名 function 代理,此前 tool_choice 过滤 把它连同其他服务端工具类型一起静默丢弃,强制工具搜索被退化为自动选择, 违反调用方明确指定的工具选择语义。改为映射到指向代理的 function 选择; 未声明 type=tool_search 时无可指向的代理,维持丢弃。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU
This commit is contained in:
@@ -724,17 +724,25 @@ func responsesToolChoiceToChatToolChoice(raw json.RawMessage, declared map[strin
|
||||
// "auto"/"none"/"required" 等字符串形式原样转发。
|
||||
return raw
|
||||
}
|
||||
// custom 工具已降级为 function 工具,指向它的 tool_choice 同样按 function 转换。
|
||||
if t := rawString(choice["type"]); t != "function" && t != "custom" {
|
||||
var name string
|
||||
switch rawString(choice["type"]) {
|
||||
case "tool_search":
|
||||
// tool_search 未被丢弃而是降级为同名 function 代理(见
|
||||
// responsesToolsToChatTools),强制选择它同样降级为 function 选择,
|
||||
// 静默丢弃会把强制搜索退化为自动选择。
|
||||
name = toolSearchProxyName
|
||||
case "function", "custom":
|
||||
// custom 工具已降级为 function 工具,指向它的 tool_choice 同样按 function 转换。
|
||||
name = rawString(choice["name"])
|
||||
if name == "" {
|
||||
name = rawNestedString(choice["function"], "name")
|
||||
}
|
||||
if name == "" {
|
||||
return raw
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
name := rawString(choice["name"])
|
||||
if name == "" {
|
||||
name = rawNestedString(choice["function"], "name")
|
||||
}
|
||||
if name == "" {
|
||||
return raw
|
||||
}
|
||||
if !declared[name] {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -621,6 +621,30 @@ func TestResponsesToChatCompletionsRequest_DropsToolChoiceForDroppedTool(t *test
|
||||
assert.JSONEq(t, `{"type":"function","function":{"name":"wait"}}`, string(out.ToolChoice))
|
||||
}
|
||||
|
||||
// tool_search 工具没有被丢弃而是降级为同名 function 代理,强制选择它的 tool_choice
|
||||
// 必须同步降级为指向代理的 function 选择,不能静默丢弃(丢弃会把强制搜索退化为
|
||||
// 自动选择,模型可以不执行搜索)。
|
||||
func TestResponsesToChatCompletionsRequest_ToolSearchToolChoiceMapsToProxy(t *testing.T) {
|
||||
out, err := ResponsesToChatCompletionsRequest(&ResponsesRequest{
|
||||
Model: "glm-5.2",
|
||||
Input: json.RawMessage(`"hi"`),
|
||||
Tools: []ResponsesTool{{Type: "tool_search"}},
|
||||
ToolChoice: json.RawMessage(`{"type":"tool_search"}`),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{"type":"function","function":{"name":"tool_search"}}`, string(out.ToolChoice))
|
||||
|
||||
// 未声明 type=tool_search 时强制选择它没有可指向的代理,丢弃选择项。
|
||||
out, err = ResponsesToChatCompletionsRequest(&ResponsesRequest{
|
||||
Model: "glm-5.2",
|
||||
Input: json.RawMessage(`"hi"`),
|
||||
Tools: []ResponsesTool{{Type: "function", Name: "wait"}},
|
||||
ToolChoice: json.RawMessage(`{"type":"tool_search"}`),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out.ToolChoice)
|
||||
}
|
||||
|
||||
// 客户端请求在原生 Responses API 上合法(namespace 子工具按 namespace+name 路由),
|
||||
// 是摊平转换让名字产生歧义;歧义无法消除时必须显式拒绝整个请求(400),而不是
|
||||
// 静默降级——否则重复声明发给上游、回程还原到错误工具,问题只能靠抓包定位。
|
||||
|
||||
Reference in New Issue
Block a user