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
264 lines
7.9 KiB
Go
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
|
|
}
|