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
213 lines
6.7 KiB
Go
213 lines
6.7 KiB
Go
package apicompat
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ResponsesNamespaceName identifies a function child in a Responses namespace.
|
|
// It aliases the chat bridge mapping so both native and bridged paths share one
|
|
// namespace identity contract.
|
|
type ResponsesNamespaceName = NamespacedToolName
|
|
|
|
// FlattenResponsesNamespaces converts Codex private namespace declarations into
|
|
// public Responses function tools and rewrites namespace-qualified request calls.
|
|
func FlattenResponsesNamespaces(req map[string]any) (map[string]ResponsesNamespaceName, bool, error) {
|
|
return FlattenResponsesNamespacesExcept(req, nil)
|
|
}
|
|
|
|
// FlattenResponsesNamespacesExcept is FlattenResponsesNamespaces with a set of
|
|
// service-owned namespace names that must remain native in the request.
|
|
func FlattenResponsesNamespacesExcept(req map[string]any, preserved map[string]bool) (map[string]ResponsesNamespaceName, bool, error) {
|
|
if req == nil {
|
|
return nil, false, nil
|
|
}
|
|
tools, ok := req["tools"].([]any)
|
|
if !ok || len(tools) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
topLevel := make(map[string]bool)
|
|
for _, raw := range tools {
|
|
tool, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
typ := strings.TrimSpace(stringValue(tool["type"]))
|
|
name := strings.TrimSpace(stringValue(tool["name"]))
|
|
if (typ == "function" || typ == "custom") && name != "" {
|
|
topLevel[name] = true
|
|
}
|
|
}
|
|
|
|
names := make(map[string]ResponsesNamespaceName)
|
|
for _, raw := range tools {
|
|
tool, ok := raw.(map[string]any)
|
|
if !ok || strings.TrimSpace(stringValue(tool["type"])) != "namespace" {
|
|
continue
|
|
}
|
|
namespace := strings.TrimSpace(stringValue(tool["name"]))
|
|
if namespace == "" || preserved[namespace] {
|
|
continue
|
|
}
|
|
for _, rawChild := range namespaceChildren(tool) {
|
|
child, ok := rawChild.(map[string]any)
|
|
if !ok || strings.TrimSpace(stringValue(child["type"])) != "function" {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(stringValue(child["name"]))
|
|
if name == "" {
|
|
continue
|
|
}
|
|
flat := flattenNamespaceToolName(namespace, name)
|
|
entry := ResponsesNamespaceName{Namespace: namespace, Name: name}
|
|
if topLevel[flat] {
|
|
return nil, false, fmt.Errorf("namespace tool %q/%q flattens to %q which conflicts with a top-level tool of the same name; this upstream cannot disambiguate them, rename one of the tools", namespace, name, flat)
|
|
}
|
|
if prev, exists := names[flat]; exists && prev != entry {
|
|
return nil, false, fmt.Errorf("namespace tools %q/%q and %q/%q both flatten to %q; this upstream cannot disambiguate them, rename one of the tools", prev.Namespace, prev.Name, namespace, name, flat)
|
|
}
|
|
names[flat] = entry
|
|
}
|
|
}
|
|
if len(names) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
flattened := make([]any, 0, len(tools)+len(names))
|
|
seen := make(map[string]bool)
|
|
for _, raw := range tools {
|
|
tool, ok := raw.(map[string]any)
|
|
if !ok || strings.TrimSpace(stringValue(tool["type"])) != "namespace" {
|
|
flattened = append(flattened, raw)
|
|
continue
|
|
}
|
|
namespace := strings.TrimSpace(stringValue(tool["name"]))
|
|
if preserved[namespace] {
|
|
flattened = append(flattened, raw)
|
|
continue
|
|
}
|
|
for _, rawChild := range namespaceChildren(tool) {
|
|
child, ok := rawChild.(map[string]any)
|
|
if !ok || strings.TrimSpace(stringValue(child["type"])) != "function" {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(stringValue(child["name"]))
|
|
flat := flattenNamespaceToolName(namespace, name)
|
|
if name == "" || seen[flat] {
|
|
continue
|
|
}
|
|
seen[flat] = true
|
|
flatChild := make(map[string]any, len(child))
|
|
for key, value := range child {
|
|
flatChild[key] = value
|
|
}
|
|
flatChild["name"] = flat
|
|
flattened = append(flattened, flatChild)
|
|
}
|
|
}
|
|
req["tools"] = flattened
|
|
rewriteNamespaceQualifiedCalls(req["input"], names)
|
|
if choice, ok := req["tool_choice"].(map[string]any); ok {
|
|
choiceNamespace := strings.TrimSpace(stringValue(choice["name"]))
|
|
if strings.TrimSpace(stringValue(choice["type"])) == "namespace" && !preserved[choiceNamespace] {
|
|
req["tool_choice"] = "auto"
|
|
} else {
|
|
rewriteNamespaceQualifiedCall(choice, names)
|
|
}
|
|
}
|
|
return names, true, nil
|
|
}
|
|
|
|
// RestoreResponsesNamespaceCalls restores flattened function calls in a JSON
|
|
// Responses payload to the namespace/name identity expected by Codex.
|
|
func RestoreResponsesNamespaceCalls(payload []byte, names map[string]ResponsesNamespaceName) ([]byte, bool, error) {
|
|
if len(payload) == 0 || len(names) == 0 {
|
|
return payload, false, nil
|
|
}
|
|
var value any
|
|
if err := json.Unmarshal(payload, &value); err != nil {
|
|
return payload, false, err
|
|
}
|
|
changed := restoreResponsesNamespaceValue(value, names)
|
|
if !changed {
|
|
return payload, false, nil
|
|
}
|
|
var rebuilt bytes.Buffer
|
|
encoder := json.NewEncoder(&rebuilt)
|
|
encoder.SetEscapeHTML(false)
|
|
if err := encoder.Encode(value); err != nil {
|
|
return payload, false, err
|
|
}
|
|
return bytes.TrimSuffix(rebuilt.Bytes(), []byte("\n")), true, nil
|
|
}
|
|
|
|
func namespaceChildren(tool map[string]any) []any {
|
|
if children, ok := tool["tools"].([]any); ok && len(children) > 0 {
|
|
return children
|
|
}
|
|
children, _ := tool["children"].([]any)
|
|
return children
|
|
}
|
|
|
|
func rewriteNamespaceQualifiedCalls(value any, names map[string]ResponsesNamespaceName) {
|
|
switch typed := value.(type) {
|
|
case []any:
|
|
for _, item := range typed {
|
|
rewriteNamespaceQualifiedCalls(item, names)
|
|
}
|
|
case map[string]any:
|
|
if strings.TrimSpace(stringValue(typed["type"])) == "function_call" {
|
|
rewriteNamespaceQualifiedCall(typed, names)
|
|
}
|
|
for _, child := range typed {
|
|
rewriteNamespaceQualifiedCalls(child, names)
|
|
}
|
|
}
|
|
}
|
|
|
|
func rewriteNamespaceQualifiedCall(item map[string]any, names map[string]ResponsesNamespaceName) bool {
|
|
namespace := strings.TrimSpace(stringValue(item["namespace"]))
|
|
name := strings.TrimSpace(stringValue(item["name"]))
|
|
if namespace == "" || name == "" {
|
|
return false
|
|
}
|
|
flat := flattenNamespaceToolName(namespace, name)
|
|
entry, ok := names[flat]
|
|
if !ok || entry.Namespace != namespace || entry.Name != name {
|
|
return false
|
|
}
|
|
item["name"] = flat
|
|
delete(item, "namespace")
|
|
return true
|
|
}
|
|
|
|
func restoreResponsesNamespaceValue(value any, names map[string]ResponsesNamespaceName) bool {
|
|
changed := false
|
|
switch typed := value.(type) {
|
|
case []any:
|
|
for _, item := range typed {
|
|
changed = restoreResponsesNamespaceValue(item, names) || changed
|
|
}
|
|
case map[string]any:
|
|
if strings.TrimSpace(stringValue(typed["type"])) == "function_call" {
|
|
if entry, ok := names[strings.TrimSpace(stringValue(typed["name"]))]; ok {
|
|
typed["name"] = entry.Name
|
|
typed["namespace"] = entry.Namespace
|
|
changed = true
|
|
}
|
|
}
|
|
for _, child := range typed {
|
|
changed = restoreResponsesNamespaceValue(child, names) || changed
|
|
}
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func stringValue(value any) string {
|
|
text, _ := value.(string)
|
|
return text
|
|
}
|