responses_namespace.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package apicompat
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "strings"
  7. )
  8. // ResponsesNamespaceName identifies a function child in a Responses namespace.
  9. // It aliases the chat bridge mapping so both native and bridged paths share one
  10. // namespace identity contract.
  11. type ResponsesNamespaceName = NamespacedToolName
  12. // FlattenResponsesNamespaces converts Codex private namespace declarations into
  13. // public Responses function tools and rewrites namespace-qualified request calls.
  14. func FlattenResponsesNamespaces(req map[string]any) (map[string]ResponsesNamespaceName, bool, error) {
  15. return FlattenResponsesNamespacesExcept(req, nil)
  16. }
  17. // FlattenResponsesNamespacesExcept is FlattenResponsesNamespaces with a set of
  18. // service-owned namespace names that must remain native in the request.
  19. func FlattenResponsesNamespacesExcept(req map[string]any, preserved map[string]bool) (map[string]ResponsesNamespaceName, bool, error) {
  20. if req == nil {
  21. return nil, false, nil
  22. }
  23. tools, ok := req["tools"].([]any)
  24. if !ok || len(tools) == 0 {
  25. return nil, false, nil
  26. }
  27. topLevel := make(map[string]bool)
  28. for _, raw := range tools {
  29. tool, ok := raw.(map[string]any)
  30. if !ok {
  31. continue
  32. }
  33. typ := strings.TrimSpace(stringValue(tool["type"]))
  34. name := strings.TrimSpace(stringValue(tool["name"]))
  35. if (typ == "function" || typ == "custom") && name != "" {
  36. topLevel[name] = true
  37. }
  38. }
  39. names := make(map[string]ResponsesNamespaceName)
  40. for _, raw := range tools {
  41. tool, ok := raw.(map[string]any)
  42. if !ok || strings.TrimSpace(stringValue(tool["type"])) != "namespace" {
  43. continue
  44. }
  45. namespace := strings.TrimSpace(stringValue(tool["name"]))
  46. if namespace == "" || preserved[namespace] {
  47. continue
  48. }
  49. for _, rawChild := range namespaceChildren(tool) {
  50. child, ok := rawChild.(map[string]any)
  51. if !ok || strings.TrimSpace(stringValue(child["type"])) != "function" {
  52. continue
  53. }
  54. name := strings.TrimSpace(stringValue(child["name"]))
  55. if name == "" {
  56. continue
  57. }
  58. flat := flattenNamespaceToolName(namespace, name)
  59. entry := ResponsesNamespaceName{Namespace: namespace, Name: name}
  60. if topLevel[flat] {
  61. 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)
  62. }
  63. if prev, exists := names[flat]; exists && prev != entry {
  64. 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)
  65. }
  66. names[flat] = entry
  67. }
  68. }
  69. if len(names) == 0 {
  70. return nil, false, nil
  71. }
  72. flattened := make([]any, 0, len(tools)+len(names))
  73. seen := make(map[string]bool)
  74. for _, raw := range tools {
  75. tool, ok := raw.(map[string]any)
  76. if !ok || strings.TrimSpace(stringValue(tool["type"])) != "namespace" {
  77. flattened = append(flattened, raw)
  78. continue
  79. }
  80. namespace := strings.TrimSpace(stringValue(tool["name"]))
  81. if preserved[namespace] {
  82. flattened = append(flattened, raw)
  83. continue
  84. }
  85. for _, rawChild := range namespaceChildren(tool) {
  86. child, ok := rawChild.(map[string]any)
  87. if !ok || strings.TrimSpace(stringValue(child["type"])) != "function" {
  88. continue
  89. }
  90. name := strings.TrimSpace(stringValue(child["name"]))
  91. flat := flattenNamespaceToolName(namespace, name)
  92. if name == "" || seen[flat] {
  93. continue
  94. }
  95. seen[flat] = true
  96. flatChild := make(map[string]any, len(child))
  97. for key, value := range child {
  98. flatChild[key] = value
  99. }
  100. flatChild["name"] = flat
  101. flattened = append(flattened, flatChild)
  102. }
  103. }
  104. req["tools"] = flattened
  105. rewriteNamespaceQualifiedCalls(req["input"], names)
  106. if choice, ok := req["tool_choice"].(map[string]any); ok {
  107. choiceNamespace := strings.TrimSpace(stringValue(choice["name"]))
  108. if strings.TrimSpace(stringValue(choice["type"])) == "namespace" && !preserved[choiceNamespace] {
  109. req["tool_choice"] = "auto"
  110. } else {
  111. rewriteNamespaceQualifiedCall(choice, names)
  112. }
  113. }
  114. return names, true, nil
  115. }
  116. // RestoreResponsesNamespaceCalls restores flattened function calls in a JSON
  117. // Responses payload to the namespace/name identity expected by Codex.
  118. func RestoreResponsesNamespaceCalls(payload []byte, names map[string]ResponsesNamespaceName) ([]byte, bool, error) {
  119. if len(payload) == 0 || len(names) == 0 {
  120. return payload, false, nil
  121. }
  122. var value any
  123. if err := json.Unmarshal(payload, &value); err != nil {
  124. return payload, false, err
  125. }
  126. changed := restoreResponsesNamespaceValue(value, names)
  127. if !changed {
  128. return payload, false, nil
  129. }
  130. var rebuilt bytes.Buffer
  131. encoder := json.NewEncoder(&rebuilt)
  132. encoder.SetEscapeHTML(false)
  133. if err := encoder.Encode(value); err != nil {
  134. return payload, false, err
  135. }
  136. return bytes.TrimSuffix(rebuilt.Bytes(), []byte("\n")), true, nil
  137. }
  138. func namespaceChildren(tool map[string]any) []any {
  139. if children, ok := tool["tools"].([]any); ok && len(children) > 0 {
  140. return children
  141. }
  142. children, _ := tool["children"].([]any)
  143. return children
  144. }
  145. func rewriteNamespaceQualifiedCalls(value any, names map[string]ResponsesNamespaceName) {
  146. switch typed := value.(type) {
  147. case []any:
  148. for _, item := range typed {
  149. rewriteNamespaceQualifiedCalls(item, names)
  150. }
  151. case map[string]any:
  152. if strings.TrimSpace(stringValue(typed["type"])) == "function_call" {
  153. rewriteNamespaceQualifiedCall(typed, names)
  154. }
  155. for _, child := range typed {
  156. rewriteNamespaceQualifiedCalls(child, names)
  157. }
  158. }
  159. }
  160. func rewriteNamespaceQualifiedCall(item map[string]any, names map[string]ResponsesNamespaceName) bool {
  161. namespace := strings.TrimSpace(stringValue(item["namespace"]))
  162. name := strings.TrimSpace(stringValue(item["name"]))
  163. if namespace == "" || name == "" {
  164. return false
  165. }
  166. flat := flattenNamespaceToolName(namespace, name)
  167. entry, ok := names[flat]
  168. if !ok || entry.Namespace != namespace || entry.Name != name {
  169. return false
  170. }
  171. item["name"] = flat
  172. delete(item, "namespace")
  173. return true
  174. }
  175. func restoreResponsesNamespaceValue(value any, names map[string]ResponsesNamespaceName) bool {
  176. changed := false
  177. switch typed := value.(type) {
  178. case []any:
  179. for _, item := range typed {
  180. changed = restoreResponsesNamespaceValue(item, names) || changed
  181. }
  182. case map[string]any:
  183. if strings.TrimSpace(stringValue(typed["type"])) == "function_call" {
  184. if entry, ok := names[strings.TrimSpace(stringValue(typed["name"]))]; ok {
  185. typed["name"] = entry.Name
  186. typed["namespace"] = entry.Namespace
  187. changed = true
  188. }
  189. }
  190. for _, child := range typed {
  191. changed = restoreResponsesNamespaceValue(child, names) || changed
  192. }
  193. }
  194. return changed
  195. }
  196. func stringValue(value any) string {
  197. text, _ := value.(string)
  198. return text
  199. }