| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- 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
- }
|