openai_mode1_semantics.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package service
  2. import "strings"
  3. // This is intentionally a small equivalence map, not a second forwarding
  4. // pipeline. Unknown fields, item types, tools and content remain comparable.
  5. func mode1CanonicalizeCodexRequest(body map[string]any) {
  6. if model, ok := body["model"].(string); ok {
  7. body["model"] = strings.TrimSpace(model)
  8. }
  9. if reasoning, ok := body["reasoning"].(map[string]any); ok {
  10. if reasoning["effort"] == "minimal" {
  11. reasoning["effort"] = "none"
  12. }
  13. model, _ := body["model"].(string)
  14. mode, _ := reasoning["mode"].(string)
  15. effort, _ := reasoning["effort"].(string)
  16. if !isOpenAIGPT6AstraModel(model) && strings.EqualFold(strings.TrimSpace(mode), "pro") && strings.TrimSpace(effort) == "" {
  17. reasoning["effort"] = "max"
  18. delete(reasoning, "mode")
  19. }
  20. }
  21. if text, ok := body["input"].(string); ok {
  22. body["input"] = []any{}
  23. if strings.TrimSpace(text) != "" {
  24. body["input"] = []any{map[string]any{"type": "message", "role": "user", "content": text}}
  25. }
  26. }
  27. // The proven OAuth path promotes text-only system messages into
  28. // instructions. Retain them as developer for JSON object requests.
  29. omitSystem := true
  30. if text, ok := body["text"].(map[string]any); ok {
  31. if format, ok := text["format"].(map[string]any); ok && format["type"] == "json_object" {
  32. omitSystem = false
  33. }
  34. }
  35. extractSystemMessagesFromInput(body, omitSystem)
  36. if instructions, ok := body["instructions"].(string); body["instructions"] == nil || (ok && strings.TrimSpace(instructions) == "") {
  37. delete(body, "instructions") // an empty instructions field may receive defaults
  38. }
  39. _, hasTools := body["tools"]
  40. if functions, ok := body["functions"].([]any); ok && !hasTools {
  41. tools := make([]any, 0, len(functions))
  42. for _, function := range functions {
  43. tools = append(tools, map[string]any{"type": "function", "function": function})
  44. }
  45. body["tools"] = tools
  46. delete(body, "functions")
  47. }
  48. _, hasChoice := body["tool_choice"]
  49. if choice, exists := body["function_call"]; exists && !hasChoice {
  50. switch c := choice.(type) {
  51. case string:
  52. body["tool_choice"] = c
  53. delete(body, "function_call")
  54. case map[string]any:
  55. if name, ok := c["name"].(string); ok && name != "" {
  56. body["tool_choice"] = map[string]any{"type": "function", "name": name}
  57. delete(body, "function_call")
  58. }
  59. }
  60. }
  61. if tools, ok := body["tools"].([]any); ok {
  62. for _, raw := range tools {
  63. tool, ok := raw.(map[string]any)
  64. if !ok || tool["type"] != "function" {
  65. continue
  66. }
  67. if function, ok := tool["function"].(map[string]any); ok {
  68. for key, value := range function {
  69. if _, exists := tool[key]; !exists {
  70. tool[key] = value
  71. }
  72. }
  73. delete(tool, "function")
  74. }
  75. }
  76. }
  77. if choice, ok := body["tool_choice"].(map[string]any); ok && choice["type"] == "function" {
  78. if function, ok := choice["function"].(map[string]any); ok {
  79. if _, exists := choice["name"]; !exists {
  80. choice["name"] = function["name"]
  81. }
  82. delete(choice, "function")
  83. }
  84. }
  85. // Aliasing reserved tool names is bijective and applied to declarations,
  86. // choices and call items together. Errors leave invalid input comparable.
  87. _, _, _ = aliasOpenAIOAuthReservedToolNames(body)
  88. input, ok := body["input"].([]any)
  89. if !ok {
  90. return
  91. }
  92. referenceIDs := codexItemReferenceIDMappings(input, false)
  93. itemIDs := codexInputItemIDs(input)
  94. callIDs := codexInputCallIDs(input)
  95. for i, raw := range input {
  96. item, ok := raw.(map[string]any)
  97. if !ok {
  98. continue
  99. }
  100. if item["role"] == "tool" && strings.TrimSpace(firstNonEmptyString(item["call_id"], item["tool_call_id"], item["id"])) != "" {
  101. if _, lossless := extractLosslessTextFromContent(item["content"]); lossless {
  102. if result, changed := normalizeCodexToolRoleMessages([]any{item}); changed {
  103. item = result[0].(map[string]any)
  104. input[i] = item
  105. }
  106. }
  107. }
  108. typ, _ := item["type"].(string)
  109. if typ == "" && (item["role"] == "user" || item["role"] == "assistant" || item["role"] == "developer") {
  110. typ = "message"
  111. item["type"] = typ
  112. }
  113. switch typ {
  114. case "message":
  115. // Message IDs are store=false replay metadata. Content and role
  116. // stay protected, including multimodal payloads and their order.
  117. delete(item, "id")
  118. if text, ok := item["content"].(string); ok {
  119. item["content"] = []any{map[string]any{"type": "input_text", "text": text}}
  120. }
  121. if parts, ok := item["content"].([]any); ok {
  122. for _, rawPart := range parts {
  123. if part, ok := rawPart.(map[string]any); ok && (part["type"] == "text" || part["type"] == "output_text") {
  124. part["type"] = "input_text"
  125. }
  126. }
  127. }
  128. case "reasoning":
  129. delete(item, "id")
  130. delete(item, "call_id")
  131. if item["summary"] == nil {
  132. item["summary"] = []any{}
  133. }
  134. case "compaction_summary":
  135. delete(item, "id")
  136. case "item_reference":
  137. id, _ := item["id"].(string)
  138. id = strings.TrimSpace(id)
  139. if _, existing := itemIDs[id]; !existing && strings.HasPrefix(id, "call_") {
  140. if mapped, exists := referenceIDs[id]; exists {
  141. item["id"] = mapped
  142. } else if _, sameTurnCall := callIDs[id]; !sameTurnCall {
  143. item["id"] = normalizeCodexCallID(id)
  144. }
  145. }
  146. }
  147. if isCodexToolCallItemType(typ) {
  148. id := firstNonEmptyString(item["call_id"], item["id"])
  149. if id != "" {
  150. item["call_id"] = normalizeCodexCallIDForItemType(typ, id)
  151. }
  152. delete(item, "id")
  153. }
  154. }
  155. }