openai_cyber_policy.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. package service
  2. import (
  3. "errors"
  4. "strings"
  5. "github.com/gin-gonic/gin"
  6. "github.com/tidwall/gjson"
  7. )
  8. // opsCyberPolicyKey 在 gin context 中携带 cyber_policy 命中标记。
  9. // 由 gateway 服务层在检测到上游 error.code=="cyber_policy" 时设置,
  10. // handler 在 Forward 返回后读取以触发风控记录、邮件与 tokens=0 用量行。
  11. const opsCyberPolicyKey = "ops_cyber_policy"
  12. // errOpenAICyberPolicyForwarded 表示 cyber_policy 已按当前端点格式透传给客户端
  13. // (error 已写出/下发)。compat 路径 ForwardAsChatCompletions / ForwardAsAnthropic 出口
  14. // 据此丢弃 result 并返回该哨兵,使 handler 落入 tokens=0 免费用量行(对齐 /v1/responses),
  15. // 既不计费、也不 failover、不重复写响应。
  16. var errOpenAICyberPolicyForwarded = errors.New("openai cyber_policy forwarded to client")
  17. // CyberPolicyMark 记录一次 cyber_policy 硬阻断的上游证据。
  18. type CyberPolicyMark struct {
  19. Code string // 固定 "cyber_policy"
  20. Message string // 上游 error.message
  21. Body string // 上游 response.failed / 400 原始 body(已截断;未脱敏,ops_error 落库由 sanitizeErrorBodyForStorage、风控日志由 redactContentModerationSecrets 统一脱敏)
  22. UpstreamStatus int // 上游 HTTP 状态(流式=200,非流式=400)
  23. UpstreamInTok int // 上游已报 input tokens(如有)
  24. UpstreamOutTok int // 上游已报 output tokens(如有)
  25. }
  26. // MarkOpsCyberPolicy 记录 cyber 标记;首个写入生效,后续忽略(同一 turn 只记一次)。
  27. // WS 多轮场景由 handler 在每个 turn 结束后调用 ClearOpsCyberPolicy 重置。
  28. func MarkOpsCyberPolicy(c *gin.Context, mark CyberPolicyMark) {
  29. if c == nil {
  30. return
  31. }
  32. if GetOpsCyberPolicy(c) != nil {
  33. return
  34. }
  35. mark.Code = "cyber_policy"
  36. mark.Message = strings.TrimSpace(mark.Message)
  37. mark.Body = strings.TrimSpace(mark.Body)
  38. c.Set(opsCyberPolicyKey, &mark)
  39. }
  40. // GetOpsCyberPolicy 返回 cyber 标记,未命中(或已被 Clear)返回 nil。
  41. func GetOpsCyberPolicy(c *gin.Context) *CyberPolicyMark {
  42. if c == nil {
  43. return nil
  44. }
  45. if v, ok := c.Get(opsCyberPolicyKey); ok {
  46. if m, ok := v.(*CyberPolicyMark); ok && m != nil {
  47. return m
  48. }
  49. }
  50. return nil
  51. }
  52. // ClearOpsCyberPolicy 清除 cyber 标记(typed-nil 覆盖;gin context 无并发安全的
  53. // 删除原语,Set 走内部锁,与异步 GetOpsCyberPolicy 不构成 data race)。
  54. // 仅 WS 多轮路径在 turn 收尾调用;HTTP 单请求路径不调用(context 随请求销毁,
  55. // 且中间件 shouldSkipOpsErrorLogForCyber 依赖标记防双写)。
  56. // WS 路径 clear 发生在中间件收尾之前,连接响应状态为 101,不触发中间件 status>=400
  57. // 落库分支,故无双写/漏写。
  58. func ClearOpsCyberPolicy(c *gin.Context) {
  59. if c == nil {
  60. return
  61. }
  62. c.Set(opsCyberPolicyKey, (*CyberPolicyMark)(nil))
  63. }
  64. // detectOpenAICyberPolicy 精确识别 cyber_policy(对齐 codex api_bridge.rs:145 /
  65. // sse/responses.rs:529)。命中返回 (true, "cyber_policy", message)。
  66. func detectOpenAICyberPolicy(payload []byte) (bool, string, string) {
  67. code := gjson.GetBytes(payload, "error.code").String()
  68. if code == "" {
  69. code = gjson.GetBytes(payload, "response.error.code").String()
  70. }
  71. if !strings.EqualFold(strings.TrimSpace(code), "cyber_policy") {
  72. return false, "", ""
  73. }
  74. msg := gjson.GetBytes(payload, "error.message").String()
  75. if msg == "" {
  76. msg = gjson.GetBytes(payload, "response.error.message").String()
  77. }
  78. return true, "cyber_policy", strings.TrimSpace(msg)
  79. }
  80. func markOpenAICyberPolicyEvent(c *gin.Context, payload []byte, upstreamStatus int, usage *OpenAIUsage) bool {
  81. hit, code, message := detectOpenAICyberPolicy(payload)
  82. if !hit {
  83. return false
  84. }
  85. mark := CyberPolicyMark{
  86. Code: code,
  87. Message: message,
  88. Body: truncateString(string(payload), 4096),
  89. UpstreamStatus: upstreamStatus,
  90. }
  91. if usage != nil {
  92. mark.UpstreamInTok = usage.InputTokens
  93. mark.UpstreamOutTok = usage.OutputTokens
  94. }
  95. MarkOpsCyberPolicy(c, mark)
  96. return true
  97. }