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,212 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user