openai_guardian_affinity.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "github.com/gin-gonic/gin"
  6. "github.com/tidwall/gjson"
  7. )
  8. const (
  9. codexAutoReviewModel = "codex-auto-review"
  10. openAISubagentHeader = "x-openai-subagent"
  11. codexParentThreadIDHeader = "x-codex-parent-thread-id"
  12. codexTurnMetadataHeader = "x-codex-turn-metadata"
  13. )
  14. type openAIGuardianParentAffinityContextKey struct{}
  15. type openAIGuardianParentAffinity struct {
  16. currentSessionHash string
  17. legacySessionHash string
  18. }
  19. // WithOpenAIGuardianParentAffinity records a Codex review request's parent
  20. // thread as a routing hint. The hint is resolved against the current group's
  21. // sticky-session namespace later; client headers never carry an account ID.
  22. func WithOpenAIGuardianParentAffinity(ctx context.Context, c *gin.Context, body []byte, requestedModel string) context.Context {
  23. if ctx == nil || c == nil || !strings.EqualFold(strings.TrimSpace(requestedModel), codexAutoReviewModel) {
  24. return ctx
  25. }
  26. headerMetadata := c.GetHeader(codexTurnMetadataHeader)
  27. bodyMetadata := openAIRequestPayloadView(body).Get("client_metadata.x-codex-turn-metadata").String()
  28. if !hasUnambiguousOpenAICodexReviewSubagent(
  29. c.GetHeader(openAISubagentHeader),
  30. codexSubagentKindFromMetadata(headerMetadata),
  31. codexSubagentKindFromMetadata(bodyMetadata),
  32. ) {
  33. return ctx
  34. }
  35. parentID := ""
  36. for _, candidate := range []string{
  37. strings.TrimSpace(c.GetHeader(codexParentThreadIDHeader)),
  38. codexParentThreadIDFromMetadata(headerMetadata),
  39. codexParentThreadIDFromMetadata(bodyMetadata),
  40. } {
  41. if candidate == "" {
  42. continue
  43. }
  44. if parentID != "" && parentID != candidate {
  45. return ctx
  46. }
  47. parentID = candidate
  48. }
  49. if parentID == "" {
  50. return ctx
  51. }
  52. currentHash, legacyHash := deriveOpenAISessionHashes(parentID)
  53. if currentHash == "" {
  54. return ctx
  55. }
  56. return context.WithValue(ctx, openAIGuardianParentAffinityContextKey{}, openAIGuardianParentAffinity{
  57. currentSessionHash: currentHash,
  58. legacySessionHash: legacyHash,
  59. })
  60. }
  61. func codexParentThreadIDFromMetadata(raw string) string {
  62. raw = strings.TrimSpace(raw)
  63. if raw == "" || !gjson.Valid(raw) {
  64. return ""
  65. }
  66. return strings.TrimSpace(gjson.Get(raw, "parent_thread_id").String())
  67. }
  68. func codexSubagentKindFromMetadata(raw string) string {
  69. raw = strings.TrimSpace(raw)
  70. if raw == "" || !gjson.Valid(raw) {
  71. return ""
  72. }
  73. return strings.TrimSpace(gjson.Get(raw, "subagent_kind").String())
  74. }
  75. func hasUnambiguousOpenAICodexReviewSubagent(candidates ...string) bool {
  76. subagent := ""
  77. for _, candidate := range candidates {
  78. candidate = strings.ToLower(strings.TrimSpace(candidate))
  79. if candidate == "" {
  80. continue
  81. }
  82. if subagent != "" && subagent != candidate {
  83. return false
  84. }
  85. subagent = candidate
  86. }
  87. return subagent == "guardian" || subagent == "review"
  88. }
  89. func openAIGuardianParentAffinityFromContext(ctx context.Context) (openAIGuardianParentAffinity, bool) {
  90. if ctx == nil {
  91. return openAIGuardianParentAffinity{}, false
  92. }
  93. affinity, ok := ctx.Value(openAIGuardianParentAffinityContextKey{}).(openAIGuardianParentAffinity)
  94. return affinity, ok && affinity.currentSessionHash != ""
  95. }
  96. func preserveOpenAIGuardianParentBinding(ctx context.Context, sessionHash string) bool {
  97. affinity, ok := openAIGuardianParentAffinityFromContext(ctx)
  98. if !ok {
  99. return false
  100. }
  101. sessionHash = strings.TrimSpace(sessionHash)
  102. return sessionHash != "" && (sessionHash == affinity.currentSessionHash || sessionHash == affinity.legacySessionHash)
  103. }
  104. func (s *OpenAIGatewayService) resolveOpenAIGuardianParentAccountID(ctx context.Context, groupID *int64) int64 {
  105. if s == nil || s.cache == nil {
  106. return 0
  107. }
  108. affinity, ok := openAIGuardianParentAffinityFromContext(ctx)
  109. if !ok {
  110. return 0
  111. }
  112. lookupCtx := withOpenAILegacySessionHash(ctx, affinity.legacySessionHash)
  113. accountID, err := s.getStickySessionAccountID(lookupCtx, groupID, affinity.currentSessionHash)
  114. if err != nil || accountID <= 0 {
  115. return 0
  116. }
  117. return accountID
  118. }