Files
sub2api/backend/internal/pkg/apicompat/responses_tool_search_discoveries.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

264 lines
7.9 KiB
Go

package apicompat
import (
"encoding/json"
"fmt"
"strings"
)
type responsesDiscoveredToolIdentity struct {
typ string
name string
namespace string
encoded string
ambiguous bool
}
// promoteResponsesToolSearchDiscoveries makes successfully discovered client
// tools callable for function-only upstreams. The original tool_search_output
// remains request history and is normalized separately; declarations are
// appended after static tools so the client's declaration order stays stable.
func promoteResponsesToolSearchDiscoveries(req map[string]any) (bool, error) {
tools, ok := req["tools"].([]any)
if !ok || len(tools) == 0 || !hasResponsesToolSearchDeclaration(tools) {
return false, nil
}
input, ok := req["input"].([]any)
if !ok || len(input) == 0 {
return false, nil
}
known := make(map[string]responsesDiscoveredToolIdentity)
for _, raw := range tools {
registerExistingResponsesToolIdentity(known, raw)
}
promoted := make([]any, 0)
for _, rawItem := range input {
item, ok := rawItem.(map[string]any)
if !ok || strings.TrimSpace(stringValue(item["type"])) != "tool_search_output" || !usableResponsesToolSearchOutput(item) {
continue
}
discoveries, ok := item["tools"].([]any)
if !ok || len(discoveries) == 0 {
continue
}
if _, err := json.Marshal(discoveries); err != nil {
continue
}
for _, rawDiscovery := range discoveries {
discovery, ok := rawDiscovery.(map[string]any)
if !ok {
continue
}
typ := strings.TrimSpace(stringValue(discovery["type"]))
switch typ {
case "function", "custom":
copy, identity, ok := responsesDirectToolDiscovery(discovery, typ)
if !ok {
continue
}
appendTool, err := admitResponsesDiscoveredTool(known, identity.name, identity)
if err != nil {
return false, err
}
if appendTool {
promoted = append(promoted, copy)
}
case "namespace":
copy, identities, ok := responsesNamespaceToolDiscovery(discovery)
if !ok {
continue
}
children := make([]any, 0, len(identities))
for _, candidate := range identities {
appendTool, err := admitResponsesDiscoveredTool(known, candidate.flat, candidate.identity)
if err != nil {
return false, err
}
if appendTool {
children = append(children, candidate.child)
}
}
if len(children) > 0 {
copy["tools"] = children
delete(copy, "children")
promoted = append(promoted, copy)
}
}
}
}
if len(promoted) == 0 {
return false, nil
}
req["tools"] = append(tools, promoted...)
return true, nil
}
func hasResponsesToolSearchDeclaration(tools []any) bool {
for _, raw := range tools {
tool, ok := raw.(map[string]any)
if ok && strings.TrimSpace(stringValue(tool["type"])) == "tool_search" {
return true
}
}
return false
}
func usableResponsesToolSearchOutput(item map[string]any) bool {
status, present := item["status"]
if !present {
return true
}
text, ok := status.(string)
return ok && strings.TrimSpace(text) == "completed"
}
func registerExistingResponsesToolIdentity(known map[string]responsesDiscoveredToolIdentity, raw any) {
tool, ok := raw.(map[string]any)
if !ok {
return
}
typ := strings.TrimSpace(stringValue(tool["type"]))
switch typ {
case "function", "custom":
_, identity, ok := responsesDirectToolDiscovery(tool, typ)
if ok {
registerExistingResponsesIdentity(known, identity.name, identity)
}
case "namespace":
_, identities, ok := responsesNamespaceToolDiscovery(tool)
if !ok {
return
}
for _, candidate := range identities {
registerExistingResponsesIdentity(known, candidate.flat, candidate.identity)
}
}
}
func registerExistingResponsesIdentity(known map[string]responsesDiscoveredToolIdentity, key string, identity responsesDiscoveredToolIdentity) {
previous, exists := known[key]
if !exists {
known[key] = identity
return
}
if !sameResponsesDiscoveredTool(previous, identity) {
previous.ambiguous = true
known[key] = previous
}
}
func admitResponsesDiscoveredTool(known map[string]responsesDiscoveredToolIdentity, key string, identity responsesDiscoveredToolIdentity) (bool, error) {
previous, exists := known[key]
if !exists {
known[key] = identity
return true, nil
}
if !previous.ambiguous && sameResponsesDiscoveredTool(previous, identity) {
return false, nil
}
return false, fmt.Errorf("discovered tool %q conflicts with an existing declaration; this upstream cannot safely disambiguate different names, namespaces, or schemas", key)
}
func sameResponsesDiscoveredTool(left, right responsesDiscoveredToolIdentity) bool {
return left.typ == right.typ && left.name == right.name && left.namespace == right.namespace && left.encoded == right.encoded
}
func responsesDirectToolDiscovery(tool map[string]any, typ string) (map[string]any, responsesDiscoveredToolIdentity, bool) {
name := strings.TrimSpace(stringValue(tool["name"]))
if name == "" {
return nil, responsesDiscoveredToolIdentity{}, false
}
copy := copyClientTool(tool)
copy["type"] = typ
copy["name"] = name
identityCopy := copy
if typ == "custom" {
identityCopy = copyClientTool(copy)
identityCopy["type"] = "function"
identityCopy["parameters"] = json.RawMessage(customToolInputSchema)
delete(identityCopy, "format")
}
encoded, err := json.Marshal(identityCopy)
if err != nil {
return nil, responsesDiscoveredToolIdentity{}, false
}
return copy, responsesDiscoveredToolIdentity{typ: typ, name: name, encoded: string(encoded)}, true
}
// restoreInheritedResponsesClientToolDeclarations reverses only the declaration
// identities recorded by ResponsesClientToolMapping. It is used when a WS
// continuation omits tools but the HTTP function upstream still needs the
// effective session declarations on every request.
func restoreInheritedResponsesClientToolDeclarations(lowered []any, mapping ResponsesClientToolMapping) []any {
restored := make([]any, 0, len(lowered))
for _, raw := range lowered {
tool, ok := raw.(map[string]any)
if !ok {
restored = append(restored, raw)
continue
}
name := strings.TrimSpace(stringValue(tool["name"]))
switch {
case mapping.ToolSearch && name == toolSearchProxyName:
restored = append(restored, map[string]any{"type": "tool_search"})
case mapping.CustomTools[name]:
copy := copyClientTool(tool)
copy["type"] = "custom"
restored = append(restored, copy)
case mapping.NamespaceTools[name].Namespace != "":
identity := mapping.NamespaceTools[name]
child := copyClientTool(tool)
child["type"] = "function"
child["name"] = identity.Name
restored = append(restored, map[string]any{
"type": "namespace", "name": identity.Namespace, "tools": []any{child},
})
default:
restored = append(restored, copyClientTool(tool))
}
}
return restored
}
type responsesNamespaceToolCandidate struct {
flat string
child map[string]any
identity responsesDiscoveredToolIdentity
}
func responsesNamespaceToolDiscovery(tool map[string]any) (map[string]any, []responsesNamespaceToolCandidate, bool) {
namespace := strings.TrimSpace(stringValue(tool["name"]))
children := namespaceChildren(tool)
if namespace == "" || len(children) == 0 {
return nil, nil, false
}
copy := copyClientTool(tool)
copy["type"] = "namespace"
copy["name"] = namespace
identities := make([]responsesNamespaceToolCandidate, 0, len(children))
for _, rawChild := range children {
child, ok := rawChild.(map[string]any)
if !ok || strings.TrimSpace(stringValue(child["type"])) != "function" {
continue
}
childCopy, direct, ok := responsesDirectToolDiscovery(child, "function")
if !ok {
continue
}
flat := flattenNamespaceToolName(namespace, direct.name)
identities = append(identities, responsesNamespaceToolCandidate{
flat: flat,
child: childCopy,
identity: responsesDiscoveredToolIdentity{
typ: "namespace", name: direct.name, namespace: namespace, encoded: direct.encoded,
},
})
}
if len(identities) == 0 {
return nil, nil, false
}
return copy, identities, true
}