mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6593 from wucm667/fix/issue-6584-chat-tool-discovery
fix(apicompat): preserve discovered tools in chat fallback
This commit is contained in:
@@ -141,6 +141,36 @@ func EffectiveResponsesTools(req *ResponsesRequest) ([]ResponsesTool, error) {
|
||||
}
|
||||
tools = append(tools, item.Tools...)
|
||||
}
|
||||
|
||||
// A completed client tool search can introduce tools for the remainder of
|
||||
// the turn. Promote them before lowering to ChatCompletions, using the same
|
||||
// dedupe, conflict, namespace, and custom-tool rules as the native adapter.
|
||||
toolsRaw, err := json.Marshal(tools)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode responses tools for discovery promotion: %w", err)
|
||||
}
|
||||
var rawTools, rawInput []any
|
||||
if err := json.Unmarshal(toolsRaw, &rawTools); err != nil {
|
||||
return nil, fmt.Errorf("decode responses tools for discovery promotion: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(inputRaw, &rawInput); err != nil {
|
||||
return nil, fmt.Errorf("parse responses input for discovery promotion: %w", err)
|
||||
}
|
||||
promoted, err := promotedResponsesToolSearchDiscoveries(rawTools, rawInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(promoted) > 0 {
|
||||
promotedRaw, err := json.Marshal(promoted)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode promoted responses tools: %w", err)
|
||||
}
|
||||
var discovered []ResponsesTool
|
||||
if err := json.Unmarshal(promotedRaw, &discovered); err != nil {
|
||||
return nil, fmt.Errorf("decode promoted responses tools: %w", err)
|
||||
}
|
||||
tools = append(tools, discovered...)
|
||||
}
|
||||
return tools, nil
|
||||
}
|
||||
|
||||
@@ -456,6 +486,11 @@ func buildChatMessagesFromItems(messages []ChatMessage, rawItems []json.RawMessa
|
||||
continue
|
||||
case "function_call_output", "custom_tool_call_output", "tool_search_output":
|
||||
outputRaw := bytesTrimSpace(item["output"])
|
||||
if itemType == "tool_search_output" && (len(outputRaw) == 0 || string(outputRaw) == "null") {
|
||||
// Newer clients return discoveries in tools[] without a separate
|
||||
// output field. Keep that useful result in Chat tool history.
|
||||
outputRaw = bytesTrimSpace(item["tools"])
|
||||
}
|
||||
callID := rawString(item["call_id"])
|
||||
if callID == "" && invalidEmptyFunctionCallOutputs > 0 {
|
||||
invalidEmptyFunctionCallOutputs--
|
||||
|
||||
@@ -597,6 +597,65 @@ func TestResponsesInputToChatMessages_ToolSearchCallHistory(t *testing.T) {
|
||||
assert.JSONEq(t, `"{\"groups\":[\"gmail\"]}"`, string(messages[2].Content))
|
||||
}
|
||||
|
||||
func TestResponsesToChatCompletionsRequest_PromotesCompletedToolSearchDiscoveries(t *testing.T) {
|
||||
var req ResponsesRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{
|
||||
"model":"gpt-5","tools":[
|
||||
{"type":"tool_search"},
|
||||
{"type":"function","name":"inspect","parameters":{"type":"object"}}
|
||||
],"input":[
|
||||
{"type":"tool_search_call","call_id":"search_1","arguments":{"query":"workspace"}},
|
||||
{"type":"tool_search_output","call_id":"search_1","status":"completed","execution":"client","tools":[
|
||||
{"type":"function","name":"inspect","parameters":{"type":"object"}},
|
||||
{"type":"custom","name":"exec"},
|
||||
{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent","parameters":{"type":"object"}}]}
|
||||
]}
|
||||
]}`), &req))
|
||||
|
||||
effective, err := EffectiveResponsesTools(&req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, effective, 4, "the identical static discovery must be deduplicated")
|
||||
assert.True(t, CustomToolNames(effective)["exec"])
|
||||
namespaces := NamespaceToolNames(effective)
|
||||
assert.Equal(t, NamespacedToolName{Namespace: "collaboration", Name: "spawn_agent"}, namespaces["collaboration__spawn_agent"])
|
||||
|
||||
chatReq, err := ResponsesToChatCompletionsRequest(&req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, chatReq.Tools, 4)
|
||||
assert.Equal(t, []string{"tool_search", "inspect", "exec", "collaboration__spawn_agent"}, []string{
|
||||
chatReq.Tools[0].Function.Name, chatReq.Tools[1].Function.Name,
|
||||
chatReq.Tools[2].Function.Name, chatReq.Tools[3].Function.Name,
|
||||
})
|
||||
require.Len(t, chatReq.Messages, 2)
|
||||
assert.Equal(t, "tool", chatReq.Messages[1].Role)
|
||||
var discoveryPayload []map[string]any
|
||||
var serialized string
|
||||
require.NoError(t, json.Unmarshal(chatReq.Messages[1].Content, &serialized))
|
||||
require.NoError(t, json.Unmarshal([]byte(serialized), &discoveryPayload))
|
||||
require.Len(t, discoveryPayload, 3)
|
||||
|
||||
responses := ChatCompletionsResponseToResponses(&ChatCompletionsResponse{Choices: []ChatChoice{{
|
||||
Message: ChatMessage{Role: "assistant", ToolCalls: []ChatToolCall{
|
||||
{ID: "call_1", Type: "function", Function: ChatFunctionCall{Name: "collaboration__spawn_agent", Arguments: `{}`}},
|
||||
{ID: "call_2", Type: "function", Function: ChatFunctionCall{Name: "exec", Arguments: `{"input":"pwd"}`}},
|
||||
}},
|
||||
}}}, req.Model, CustomToolNames(effective), FunctionToolNames(effective), HasToolSearchTool(effective), namespaces)
|
||||
require.Len(t, responses.Output, 2)
|
||||
assert.Equal(t, "function_call", responses.Output[0].Type)
|
||||
assert.Equal(t, "spawn_agent", responses.Output[0].Name)
|
||||
assert.Equal(t, "collaboration", responses.Output[0].Namespace)
|
||||
assert.Equal(t, "custom_tool_call", responses.Output[1].Type)
|
||||
assert.Equal(t, "exec", responses.Output[1].Name)
|
||||
assert.Equal(t, "pwd", responses.Output[1].Input)
|
||||
}
|
||||
|
||||
func TestEffectiveResponsesTools_RejectsDiscoveredConflict(t *testing.T) {
|
||||
var req ResponsesRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"tools":[{"type":"tool_search"},{"type":"function","name":"inspect","parameters":{"type":"object"}}],"input":[{"type":"tool_search_output","status":"completed","tools":[{"type":"function","name":"inspect","parameters":{"type":"string"}}]}]}`), &req))
|
||||
_, err := EffectiveResponsesTools(&req)
|
||||
require.ErrorContains(t, err, "conflicts")
|
||||
}
|
||||
|
||||
func TestResponsesInputToChatMessages_NamespacedFunctionCallHistory(t *testing.T) {
|
||||
input := json.RawMessage(`[
|
||||
{"type":"function_call","call_id":"call_n","name":"send","namespace":"gmail","arguments":"{\"to\":\"a\"}"},
|
||||
|
||||
@@ -27,6 +27,21 @@ func promoteResponsesToolSearchDiscoveries(req map[string]any) (bool, error) {
|
||||
if !ok || len(input) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
promoted, err := promotedResponsesToolSearchDiscoveries(tools, input)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(promoted) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
req["tools"] = append(tools, promoted...)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func promotedResponsesToolSearchDiscoveries(tools, input []any) ([]any, error) {
|
||||
if len(tools) == 0 || !hasResponsesToolSearchDeclaration(tools) || len(input) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
known := make(map[string]responsesDiscoveredToolIdentity)
|
||||
for _, raw := range tools {
|
||||
@@ -60,7 +75,7 @@ func promoteResponsesToolSearchDiscoveries(req map[string]any) (bool, error) {
|
||||
}
|
||||
appendTool, err := admitResponsesDiscoveredTool(known, identity.name, identity)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
if appendTool {
|
||||
promoted = append(promoted, copy)
|
||||
@@ -74,7 +89,7 @@ func promoteResponsesToolSearchDiscoveries(req map[string]any) (bool, error) {
|
||||
for _, candidate := range identities {
|
||||
appendTool, err := admitResponsesDiscoveredTool(known, candidate.flat, candidate.identity)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
if appendTool {
|
||||
children = append(children, candidate.child)
|
||||
@@ -88,11 +103,7 @@ func promoteResponsesToolSearchDiscoveries(req map[string]any) (bool, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(promoted) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
req["tools"] = append(tools, promoted...)
|
||||
return true, nil
|
||||
return promoted, nil
|
||||
}
|
||||
|
||||
func hasResponsesToolSearchDeclaration(tools []any) bool {
|
||||
|
||||
Reference in New Issue
Block a user