Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
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
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
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user