content_moderation_input.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. package service
  2. import (
  3. "crypto/rand"
  4. "fmt"
  5. "math/big"
  6. "strings"
  7. "github.com/tidwall/gjson"
  8. )
  9. func ExtractContentModerationText(protocol string, body []byte) string {
  10. return ExtractContentModerationInput(protocol, body).Text
  11. }
  12. func ExtractContentModerationInput(protocol string, body []byte) ContentModerationInput {
  13. if len(body) == 0 || !gjson.ValidBytes(body) {
  14. return ContentModerationInput{}
  15. }
  16. var parts []string
  17. var images []string
  18. switch protocol {
  19. case ContentModerationProtocolAnthropicMessages:
  20. collectLastAnthropicUserMessage(gjson.GetBytes(body, "messages"), &parts, &images)
  21. case ContentModerationProtocolOpenAIChat:
  22. collectLastRoleMessage(gjson.GetBytes(body, "messages"), "user", &parts, &images)
  23. case ContentModerationProtocolOpenAIResponses:
  24. collectLastResponsesInput(gjson.GetBytes(body, "input"), &parts, &images)
  25. case ContentModerationProtocolGemini:
  26. collectLastGeminiContent(gjson.GetBytes(body, "contents"), &parts, &images)
  27. case ContentModerationProtocolOpenAIImages:
  28. addModerationText(&parts, gjson.GetBytes(body, "prompt").String())
  29. collectContentValue(gjson.GetBytes(body, "images"), &parts, &images)
  30. default:
  31. collectLastResponsesInput(gjson.GetBytes(body, "input"), &parts, &images)
  32. collectLastRoleMessage(gjson.GetBytes(body, "messages"), "user", &parts, &images)
  33. collectLastGeminiContent(gjson.GetBytes(body, "contents"), &parts, &images)
  34. }
  35. out := ContentModerationInput{
  36. Text: normalizeContentModerationText(strings.Join(parts, "\n")),
  37. Images: normalizeModerationImages(images),
  38. }
  39. out.Normalize()
  40. return out
  41. }
  42. func collectLastRoleMessage(messages gjson.Result, role string, parts *[]string, images *[]string) {
  43. if !messages.IsArray() {
  44. return
  45. }
  46. array := messages.Array()
  47. if len(array) == 0 {
  48. return
  49. }
  50. last := array[len(array)-1]
  51. if strings.ToLower(strings.TrimSpace(last.Get("role").String())) != role {
  52. return
  53. }
  54. var candidate []string
  55. var candidateImages []string
  56. collectContentValue(last.Get("content"), &candidate, &candidateImages)
  57. if normalizeContentModerationText(strings.Join(candidate, "\n")) == "" && len(candidateImages) == 0 {
  58. return
  59. }
  60. *parts = append(*parts, candidate...)
  61. *images = append(*images, candidateImages...)
  62. }
  63. func collectLastAnthropicUserMessage(messages gjson.Result, parts *[]string, images *[]string) {
  64. if !messages.IsArray() {
  65. return
  66. }
  67. array := messages.Array()
  68. if len(array) == 0 {
  69. return
  70. }
  71. last := array[len(array)-1]
  72. if strings.ToLower(strings.TrimSpace(last.Get("role").String())) != "user" {
  73. return
  74. }
  75. var candidate []string
  76. var candidateImages []string
  77. collectAnthropicUserContentValue(last.Get("content"), &candidate, &candidateImages)
  78. if normalizeContentModerationText(strings.Join(candidate, "\n")) == "" && len(candidateImages) == 0 {
  79. return
  80. }
  81. *parts = append(*parts, candidate...)
  82. *images = append(*images, candidateImages...)
  83. }
  84. func collectAnthropicUserContentValue(value gjson.Result, parts *[]string, images *[]string) {
  85. switch {
  86. case !value.Exists():
  87. return
  88. case value.Type == gjson.String:
  89. if !isAnthropicSystemReminderText(value.String()) {
  90. addModerationText(parts, value.String())
  91. }
  92. case value.IsArray():
  93. value.ForEach(func(_, item gjson.Result) bool {
  94. collectAnthropicUserContentValue(item, parts, images)
  95. return true
  96. })
  97. case value.IsObject():
  98. typ := strings.ToLower(strings.TrimSpace(value.Get("type").String()))
  99. switch typ {
  100. case "", "text", "input_text", "message":
  101. if value.Get("text").Exists() && !isAnthropicSystemReminderText(value.Get("text").String()) {
  102. addModerationText(parts, value.Get("text").String())
  103. }
  104. if value.Get("content").Exists() {
  105. collectAnthropicUserContentValue(value.Get("content"), parts, images)
  106. }
  107. case "image_url", "input_image", "image":
  108. collectContentValue(value, parts, images)
  109. }
  110. }
  111. }
  112. func isAnthropicSystemReminderText(text string) bool {
  113. return strings.HasPrefix(strings.TrimSpace(text), "<system-reminder>")
  114. }
  115. func collectLastResponsesInput(input gjson.Result, parts *[]string, images *[]string) {
  116. switch {
  117. case !input.Exists():
  118. return
  119. case input.Type == gjson.String:
  120. addModerationText(parts, input.String())
  121. case input.IsArray():
  122. array := input.Array()
  123. if len(array) == 0 {
  124. return
  125. }
  126. last := array[len(array)-1]
  127. if !isResponsesUserTextItem(last) {
  128. return
  129. }
  130. collectContentValue(last.Get("content"), parts, images)
  131. if last.Get("type").String() == "input_text" || last.Get("text").Exists() {
  132. collectContentValue(last, parts, images)
  133. }
  134. case input.IsObject():
  135. if isResponsesUserTextItem(input) {
  136. collectContentValue(input.Get("content"), parts, images)
  137. if input.Get("type").String() == "input_text" || input.Get("text").Exists() {
  138. collectContentValue(input, parts, images)
  139. }
  140. }
  141. }
  142. }
  143. func isResponsesUserTextItem(item gjson.Result) bool {
  144. role := strings.ToLower(strings.TrimSpace(item.Get("role").String()))
  145. if role == "user" {
  146. return responseItemHasModerationText(item)
  147. }
  148. if role != "" {
  149. return false
  150. }
  151. return responseItemHasModerationText(item)
  152. }
  153. func responseItemHasModerationText(item gjson.Result) bool {
  154. var parts []string
  155. var images []string
  156. collectContentValue(item.Get("content"), &parts, &images)
  157. if item.Get("type").String() == "input_text" || item.Get("text").Exists() {
  158. collectContentValue(item, &parts, &images)
  159. }
  160. return normalizeContentModerationText(strings.Join(parts, "\n")) != "" || len(images) > 0
  161. }
  162. func collectLastGeminiContent(contents gjson.Result, parts *[]string, images *[]string) {
  163. if !contents.IsArray() {
  164. return
  165. }
  166. array := contents.Array()
  167. if len(array) == 0 {
  168. return
  169. }
  170. last := array[len(array)-1]
  171. role := strings.ToLower(strings.TrimSpace(last.Get("role").String()))
  172. if role != "" && role != "user" {
  173. return
  174. }
  175. var candidate []string
  176. var candidateImages []string
  177. if arr := last.Get("parts"); arr.IsArray() {
  178. arr.ForEach(func(_, part gjson.Result) bool {
  179. addModerationText(&candidate, part.Get("text").String())
  180. addGeminiModerationImage(&candidateImages, part)
  181. return true
  182. })
  183. }
  184. if normalizeContentModerationText(strings.Join(candidate, "\n")) == "" && len(candidateImages) == 0 {
  185. return
  186. }
  187. *parts = append(*parts, candidate...)
  188. *images = append(*images, candidateImages...)
  189. }
  190. func collectContentValue(value gjson.Result, parts *[]string, images *[]string) {
  191. switch {
  192. case !value.Exists():
  193. return
  194. case value.Type == gjson.String:
  195. addModerationText(parts, value.String())
  196. case value.IsArray():
  197. value.ForEach(func(_, item gjson.Result) bool {
  198. collectContentValue(item, parts, images)
  199. return true
  200. })
  201. case value.IsObject():
  202. typ := strings.ToLower(strings.TrimSpace(value.Get("type").String()))
  203. addModerationImage(images, value.Get("image_url.url").String())
  204. addModerationImage(images, value.Get("image_url").String())
  205. addModerationImage(images, value.Get("url").String())
  206. addModerationImageData(images, value.Get("source.media_type").String(), value.Get("source.data").String())
  207. addModerationImageData(images, value.Get("source.mediaType").String(), value.Get("source.data").String())
  208. addModerationImageData(images, value.Get("media_type").String(), value.Get("data").String())
  209. addModerationImageData(images, value.Get("mime_type").String(), value.Get("data").String())
  210. addModerationImageData(images, value.Get("mimeType").String(), value.Get("data").String())
  211. addModerationImage(images, value.Get("source.data").String())
  212. addModerationImage(images, value.Get("data").String())
  213. addModerationImage(images, value.Get("base64").String())
  214. switch typ {
  215. case "", "text", "input_text", "message":
  216. if value.Get("text").Exists() {
  217. addModerationText(parts, value.Get("text").String())
  218. }
  219. if value.Get("content").Exists() {
  220. collectContentValue(value.Get("content"), parts, images)
  221. }
  222. case "image_url", "input_image", "image":
  223. }
  224. }
  225. }
  226. func addGeminiModerationImage(images *[]string, part gjson.Result) {
  227. if inlineData := part.Get("inline_data"); inlineData.IsObject() {
  228. mimeType := strings.TrimSpace(inlineData.Get("mime_type").String())
  229. data := strings.TrimSpace(inlineData.Get("data").String())
  230. if mimeType != "" && data != "" {
  231. addModerationImage(images, fmt.Sprintf("data:%s;base64,%s", mimeType, data))
  232. }
  233. }
  234. if inlineData := part.Get("inlineData"); inlineData.IsObject() {
  235. mimeType := strings.TrimSpace(inlineData.Get("mimeType").String())
  236. data := strings.TrimSpace(inlineData.Get("data").String())
  237. if mimeType != "" && data != "" {
  238. addModerationImage(images, fmt.Sprintf("data:%s;base64,%s", mimeType, data))
  239. }
  240. }
  241. addModerationImage(images, part.Get("file_data.file_uri").String())
  242. addModerationImage(images, part.Get("fileData.fileUri").String())
  243. }
  244. func addModerationImageData(images *[]string, mimeType string, data string) {
  245. mimeType = strings.TrimSpace(mimeType)
  246. data = strings.TrimSpace(data)
  247. if mimeType == "" || data == "" {
  248. return
  249. }
  250. addModerationImage(images, fmt.Sprintf("data:%s;base64,%s", mimeType, data))
  251. }
  252. func addModerationImage(images *[]string, image string) {
  253. image = strings.TrimSpace(image)
  254. if image == "" {
  255. return
  256. }
  257. if strings.HasPrefix(image, "data:") || strings.HasPrefix(image, "http://") || strings.HasPrefix(image, "https://") {
  258. *images = append(*images, image)
  259. }
  260. }
  261. func normalizeModerationImages(images []string) []string {
  262. out := make([]string, 0, len(images))
  263. seen := make(map[string]struct{}, len(images))
  264. for _, image := range images {
  265. image = strings.TrimSpace(image)
  266. if image == "" {
  267. continue
  268. }
  269. if _, ok := seen[image]; ok {
  270. continue
  271. }
  272. seen[image] = struct{}{}
  273. out = append(out, image)
  274. }
  275. return out
  276. }
  277. func limitContentModerationImages(images []string) []string {
  278. if len(images) <= maxContentModerationInputImages {
  279. return images
  280. }
  281. idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(images))))
  282. if err != nil {
  283. return images[:maxContentModerationInputImages]
  284. }
  285. return []string{images[int(idx.Int64())]}
  286. }
  287. func addModerationText(parts *[]string, text string) {
  288. text = strings.TrimSpace(text)
  289. if text == "" {
  290. return
  291. }
  292. if strings.Contains(text, "<system-reminder>") {
  293. return
  294. }
  295. *parts = append(*parts, text)
  296. }
  297. func normalizeContentModerationText(text string) string {
  298. return strings.Join(strings.Fields(strings.TrimSpace(text)), " ")
  299. }