openai_codex_tool_names.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "github.com/gin-gonic/gin"
  7. "github.com/tidwall/gjson"
  8. "github.com/tidwall/sjson"
  9. )
  10. const (
  11. codexReservedPythonToolName = "python"
  12. codexPythonToolAlias = "python__sub2api"
  13. codexToolNameReverseKey = "openai_codex_tool_name_reverse"
  14. codexToolNameSessionKey = "openai_codex_tool_name_session_reverse"
  15. )
  16. type codexToolNameField struct {
  17. object map[string]any
  18. key string
  19. name string
  20. }
  21. // aliasOpenAIOAuthReservedToolNames avoids names reserved by the ChatGPT
  22. // Codex backend. It validates every declaration/reference before mutating so
  23. // collisions cannot leave a partially rewritten request.
  24. func aliasOpenAIOAuthReservedToolNames(reqBody map[string]any) (map[string]string, bool, error) {
  25. if reqBody == nil {
  26. return nil, false, nil
  27. }
  28. fields := collectOpenAIResponsesToolNameFields(reqBody)
  29. owners := make(map[string]string)
  30. reverse := make(map[string]string)
  31. for _, field := range fields {
  32. normalized := aliasOpenAIOAuthReservedToolName(field.name)
  33. original := field.name
  34. if normalized != field.name {
  35. original = strings.TrimSpace(field.name)
  36. }
  37. if previous, exists := owners[normalized]; exists && previous != original {
  38. return nil, false, fmt.Errorf("tool names %q and %q both normalize to %q", previous, original, normalized)
  39. }
  40. owners[normalized] = original
  41. if normalized != field.name {
  42. reverse[normalized] = original
  43. }
  44. }
  45. if len(reverse) == 0 {
  46. return nil, false, nil
  47. }
  48. for _, field := range fields {
  49. if aliased := aliasOpenAIOAuthReservedToolName(field.name); aliased != field.name {
  50. field.object[field.key] = aliased
  51. }
  52. }
  53. return reverse, true, nil
  54. }
  55. func aliasOpenAIOAuthReservedToolName(name string) string {
  56. trimmed := strings.TrimSpace(name)
  57. if strings.EqualFold(trimmed, codexReservedPythonToolName) {
  58. return codexPythonToolAlias
  59. }
  60. return name
  61. }
  62. func collectOpenAIResponsesToolNameFields(reqBody map[string]any) []codexToolNameField {
  63. fields := make([]codexToolNameField, 0, 8)
  64. appendName := func(object map[string]any, key string) {
  65. if object == nil {
  66. return
  67. }
  68. name, ok := object[key].(string)
  69. if !ok || strings.TrimSpace(name) == "" {
  70. return
  71. }
  72. fields = append(fields, codexToolNameField{object: object, key: key, name: name})
  73. }
  74. var collectTools func(any)
  75. collectTools = func(rawTools any) {
  76. tools, ok := rawTools.([]any)
  77. if !ok {
  78. return
  79. }
  80. for _, raw := range tools {
  81. tool, ok := raw.(map[string]any)
  82. if !ok {
  83. continue
  84. }
  85. toolType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(tool["type"])))
  86. if toolType == "function" {
  87. appendName(tool, "name")
  88. if function, ok := tool["function"].(map[string]any); ok {
  89. appendName(function, "name")
  90. }
  91. }
  92. if toolType == "namespace" {
  93. collectTools(tool["tools"])
  94. }
  95. }
  96. }
  97. collectTools(reqBody["tools"])
  98. if functions, ok := reqBody["functions"].([]any); ok {
  99. for _, raw := range functions {
  100. function, _ := raw.(map[string]any)
  101. appendName(function, "name")
  102. }
  103. }
  104. if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(reqBody["type"])), "session.update") {
  105. if session, ok := reqBody["session"].(map[string]any); ok {
  106. collectTools(session["tools"])
  107. }
  108. }
  109. if choice, ok := reqBody["tool_choice"].(map[string]any); ok {
  110. if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(choice["type"])), "allowed_tools") {
  111. collectTools(choice["tools"])
  112. }
  113. if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(choice["type"])), "function") {
  114. appendName(choice, "name")
  115. if function, ok := choice["function"].(map[string]any); ok {
  116. appendName(function, "name")
  117. }
  118. }
  119. }
  120. if input, ok := reqBody["input"].([]any); ok {
  121. for _, raw := range input {
  122. item, ok := raw.(map[string]any)
  123. if !ok {
  124. continue
  125. }
  126. typ := strings.ToLower(strings.TrimSpace(firstNonEmptyString(item["type"])))
  127. if typ == "additional_tools" {
  128. collectTools(item["tools"])
  129. }
  130. if typ == "function_call" {
  131. appendName(item, "name")
  132. if function, ok := item["function"].(map[string]any); ok {
  133. appendName(function, "name")
  134. }
  135. }
  136. }
  137. }
  138. return fields
  139. }
  140. func aliasOpenAIOAuthReservedToolNamesBody(body []byte) ([]byte, map[string]string, bool, error) {
  141. if len(body) == 0 || !containsASCIIFold(body, []byte(codexReservedPythonToolName)) {
  142. return body, nil, false, nil
  143. }
  144. var reqBody map[string]any
  145. if err := decodeOpenAIJSONUseNumber(body, &reqBody); err != nil {
  146. return body, nil, false, fmt.Errorf("decode OAuth reserved tool names: %w", err)
  147. }
  148. reverse, changed, err := aliasOpenAIOAuthReservedToolNames(reqBody)
  149. if err != nil || !changed {
  150. return body, reverse, false, err
  151. }
  152. normalized, err := json.Marshal(reqBody)
  153. if err != nil {
  154. return body, nil, false, fmt.Errorf("encode OAuth reserved tool names: %w", err)
  155. }
  156. return normalized, reverse, true, nil
  157. }
  158. func containsASCIIFold(haystack, needle []byte) bool {
  159. if len(needle) == 0 || len(haystack) < len(needle) {
  160. return false
  161. }
  162. for i := 0; i <= len(haystack)-len(needle); i++ {
  163. matched := true
  164. for j := range needle {
  165. a, b := haystack[i+j], needle[j]
  166. if a >= 'A' && a <= 'Z' {
  167. a += 'a' - 'A'
  168. }
  169. if b >= 'A' && b <= 'Z' {
  170. b += 'a' - 'A'
  171. }
  172. if a != b {
  173. matched = false
  174. break
  175. }
  176. }
  177. if matched {
  178. return true
  179. }
  180. }
  181. return false
  182. }
  183. func setCodexToolNameReverse(c *gin.Context, reverse map[string]string) {
  184. if c == nil {
  185. return
  186. }
  187. storeCodexToolNameReverse(c, codexToolNameReverseKey, reverse)
  188. storeCodexToolNameReverse(c, codexToolNameSessionKey, nil)
  189. }
  190. func storeCodexToolNameReverse(c *gin.Context, key string, reverse map[string]string) {
  191. if c == nil {
  192. return
  193. }
  194. copyMap := make(map[string]string, len(reverse))
  195. for aliased, original := range reverse {
  196. copyMap[aliased] = original
  197. }
  198. c.Set(key, copyMap)
  199. }
  200. func mergeCodexToolNameReverse(c *gin.Context, reverse map[string]string) {
  201. if c == nil || len(reverse) == 0 {
  202. return
  203. }
  204. merged := make(map[string]string, len(reverse)+len(codexToolNameReverseFromContext(c)))
  205. for aliased, original := range codexToolNameReverseFromContext(c) {
  206. merged[aliased] = original
  207. }
  208. for aliased, original := range reverse {
  209. merged[aliased] = original
  210. }
  211. storeCodexToolNameReverse(c, codexToolNameReverseKey, merged)
  212. }
  213. func codexToolNameReverseFromContext(c *gin.Context) map[string]string {
  214. return codexToolNameReverseForKey(c, codexToolNameReverseKey)
  215. }
  216. func codexToolNameReverseForKey(c *gin.Context, key string) map[string]string {
  217. if c == nil {
  218. return nil
  219. }
  220. raw, ok := c.Get(key)
  221. if !ok {
  222. return nil
  223. }
  224. reverse, _ := raw.(map[string]string)
  225. return reverse
  226. }
  227. // updateCodexToolNameReverseForWSFrame keeps the active turn isolated from
  228. // session updates that may arrive while that turn is still streaming.
  229. func updateCodexToolNameReverseForWSFrame(c *gin.Context, frame []byte, reverse map[string]string) {
  230. if c == nil {
  231. return
  232. }
  233. eventType := strings.TrimSpace(gjson.GetBytes(frame, "type").String())
  234. switch eventType {
  235. case "session.update":
  236. if gjson.GetBytes(frame, "session.tools").Exists() {
  237. storeCodexToolNameReverse(c, codexToolNameSessionKey, reverse)
  238. }
  239. case "response.create", "":
  240. active := reverse
  241. if !openAIWSFrameHasExplicitToolDeclarations(frame) {
  242. active = mergeCodexToolNameReverseMaps(
  243. codexToolNameReverseForKey(c, codexToolNameSessionKey),
  244. reverse,
  245. )
  246. }
  247. storeCodexToolNameReverse(c, codexToolNameReverseKey, active)
  248. }
  249. }
  250. func openAIWSFrameHasExplicitToolDeclarations(frame []byte) bool {
  251. if gjson.GetBytes(frame, "tools").Exists() {
  252. return true
  253. }
  254. for _, item := range gjson.GetBytes(frame, "input").Array() {
  255. if strings.EqualFold(strings.TrimSpace(item.Get("type").String()), "additional_tools") && item.Get("tools").Exists() {
  256. return true
  257. }
  258. }
  259. return false
  260. }
  261. func mergeCodexToolNameReverseMaps(base, overlay map[string]string) map[string]string {
  262. merged := make(map[string]string, len(base)+len(overlay))
  263. for aliased, original := range base {
  264. merged[aliased] = original
  265. }
  266. for aliased, original := range overlay {
  267. merged[aliased] = original
  268. }
  269. return merged
  270. }
  271. func restoreCodexToolNamesInJSON(data []byte, reverse map[string]string) []byte {
  272. if len(data) == 0 || len(reverse) == 0 || !json.Valid(data) {
  273. return data
  274. }
  275. var decoded any
  276. if err := decodeOpenAIJSONUseNumber(data, &decoded); err != nil {
  277. return data
  278. }
  279. if !restoreCodexToolNameFields(decoded, reverse) {
  280. return data
  281. }
  282. restored, err := json.Marshal(decoded)
  283. if err != nil {
  284. return data
  285. }
  286. return restored
  287. }
  288. func restoreCodexToolNamesFromContext(c *gin.Context, data []byte) []byte {
  289. reverse := codexToolNameReverseFromContext(c)
  290. switch strings.TrimSpace(gjson.GetBytes(data, "type").String()) {
  291. case "session.created", "session.updated":
  292. reverse = codexToolNameReverseForKey(c, codexToolNameSessionKey)
  293. }
  294. return restoreCodexToolNamesInJSON(data, reverse)
  295. }
  296. func restoreCodexToolNamesFromSSEContext(c *gin.Context, data []byte, eventType string) []byte {
  297. if strings.TrimSpace(gjson.GetBytes(data, "type").String()) != "" || strings.TrimSpace(eventType) == "" {
  298. return restoreCodexToolNamesFromContext(c, data)
  299. }
  300. compat := []byte(openAICompatPayloadWithEventType(string(data), eventType))
  301. restored := restoreCodexToolNamesFromContext(c, compat)
  302. if string(restored) == string(compat) {
  303. return data
  304. }
  305. withoutSyntheticType, err := sjson.DeleteBytes(restored, "type")
  306. if err != nil {
  307. return restored
  308. }
  309. return withoutSyntheticType
  310. }
  311. func restoreCodexToolNameFields(value any, reverse map[string]string) bool {
  312. root, ok := value.(map[string]any)
  313. if !ok {
  314. return false
  315. }
  316. changed := false
  317. restoreItem := func(raw any) {
  318. item, ok := raw.(map[string]any)
  319. if !ok || !strings.EqualFold(strings.TrimSpace(firstNonEmptyString(item["type"])), "function_call") {
  320. return
  321. }
  322. name, _ := item["name"].(string)
  323. if original, exists := reverse[name]; exists {
  324. item["name"] = original
  325. changed = true
  326. }
  327. }
  328. restoreOutput := func(raw any) {
  329. output, _ := raw.([]any)
  330. for _, item := range output {
  331. restoreItem(item)
  332. }
  333. }
  334. restoreResponse := func(raw any) {
  335. response, ok := raw.(map[string]any)
  336. if ok {
  337. restoreOutput(response["output"])
  338. }
  339. }
  340. restoreFunction := func(raw any) {
  341. function, ok := raw.(map[string]any)
  342. if !ok {
  343. return
  344. }
  345. name, _ := function["name"].(string)
  346. if original, exists := reverse[name]; exists {
  347. function["name"] = original
  348. changed = true
  349. }
  350. }
  351. restoreChatToolCalls := func(raw any) {
  352. toolCalls, _ := raw.([]any)
  353. for _, rawCall := range toolCalls {
  354. call, ok := rawCall.(map[string]any)
  355. if !ok {
  356. continue
  357. }
  358. callType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(call["type"])))
  359. if callType == "" || callType == "function" {
  360. restoreFunction(call["function"])
  361. }
  362. }
  363. }
  364. restoreMessageContent := func(raw any) {
  365. content, _ := raw.([]any)
  366. for _, rawBlock := range content {
  367. block, ok := rawBlock.(map[string]any)
  368. if !ok || !strings.EqualFold(strings.TrimSpace(firstNonEmptyString(block["type"])), "tool_use") {
  369. continue
  370. }
  371. name, _ := block["name"].(string)
  372. if original, exists := reverse[name]; exists {
  373. block["name"] = original
  374. changed = true
  375. }
  376. }
  377. }
  378. var restoreTools func(any)
  379. restoreTools = func(raw any) {
  380. tools, _ := raw.([]any)
  381. for _, rawTool := range tools {
  382. tool, ok := rawTool.(map[string]any)
  383. if !ok {
  384. continue
  385. }
  386. toolType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(tool["type"])))
  387. if toolType == "function" {
  388. name, _ := tool["name"].(string)
  389. if original, exists := reverse[name]; exists {
  390. tool["name"] = original
  391. changed = true
  392. }
  393. }
  394. if toolType == "namespace" {
  395. restoreTools(tool["tools"])
  396. }
  397. }
  398. }
  399. eventType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(root["type"])))
  400. switch eventType {
  401. case "response.output_item.added", "response.output_item.done":
  402. restoreItem(root["item"])
  403. case "response.created", "response.in_progress", "response.completed", "response.done", "response.failed", "response.incomplete", "response.cancelled", "response.canceled":
  404. restoreResponse(root["response"])
  405. case "session.created", "session.updated":
  406. if session, ok := root["session"].(map[string]any); ok {
  407. restoreTools(session["tools"])
  408. }
  409. }
  410. if _, hasOutput := root["output"]; hasOutput {
  411. restoreOutput(root["output"])
  412. }
  413. if choices, ok := root["choices"].([]any); ok {
  414. for _, rawChoice := range choices {
  415. choice, ok := rawChoice.(map[string]any)
  416. if !ok {
  417. continue
  418. }
  419. for _, key := range []string{"message", "delta"} {
  420. if message, ok := choice[key].(map[string]any); ok {
  421. restoreChatToolCalls(message["tool_calls"])
  422. }
  423. }
  424. }
  425. }
  426. restoreMessageContent(root["content"])
  427. if block, ok := root["content_block"].(map[string]any); ok && strings.EqualFold(strings.TrimSpace(firstNonEmptyString(block["type"])), "tool_use") {
  428. name, _ := block["name"].(string)
  429. if original, exists := reverse[name]; exists {
  430. block["name"] = original
  431. changed = true
  432. }
  433. }
  434. return changed
  435. }