openai_cyber_transcript.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package service
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "encoding/json"
  6. "strconv"
  7. "strings"
  8. "github.com/tidwall/gjson"
  9. )
  10. type openAICyberTranscriptBlockKeys struct {
  11. lookupKeys []string
  12. preLatestUserKey string
  13. lookupKeysTruncated bool
  14. }
  15. // Bound the Redis lookup work for a single request while retaining the most
  16. // recent transcript prefixes, where a continuation is most likely to match.
  17. const maxOpenAICyberTranscriptLookupKeys = 256
  18. // deriveOpenAICyberTranscriptBlockKeys returns cumulative semantic-history
  19. // hashes plus the context key immediately before the latest user turn. The
  20. // context key requires model-generated history so shared first-turn templates
  21. // cannot block unrelated conversations.
  22. func deriveOpenAICyberTranscriptBlockKeys(apiKeyID int64, body []byte) openAICyberTranscriptBlockKeys {
  23. if len(body) == 0 {
  24. return openAICyberTranscriptBlockKeys{}
  25. }
  26. root := openAIRequestPayloadView(body)
  27. if !root.Exists() || !root.IsObject() {
  28. return openAICyberTranscriptBlockKeys{}
  29. }
  30. h := sha256.New()
  31. _, _ = h.Write([]byte("cyber-transcript:v3|api_key="))
  32. _, _ = h.Write([]byte(strconv.FormatInt(apiKeyID, 10)))
  33. // Model and tool definitions are request configuration rather than history.
  34. // Root instructions remain model-visible context and participate in identity.
  35. for _, field := range []string{"instructions"} {
  36. v := root.Get(field)
  37. if !v.Exists() || (v.Type == gjson.String && strings.TrimSpace(v.String()) == "") {
  38. continue
  39. }
  40. canonical := normalizeCompatSeedJSON(json.RawMessage(v.Raw))
  41. if v.Type == gjson.String {
  42. canonical = v.String()
  43. }
  44. _, _ = h.Write([]byte("|"))
  45. _, _ = h.Write([]byte(field))
  46. _, _ = h.Write([]byte("="))
  47. _, _ = h.Write([]byte(canonical))
  48. }
  49. appendSequence := func(sequence gjson.Result) openAICyberTranscriptBlockKeys {
  50. if !sequence.Exists() || !sequence.IsArray() {
  51. return openAICyberTranscriptBlockKeys{}
  52. }
  53. result := openAICyberTranscriptBlockKeys{
  54. lookupKeys: make([]string, 0, maxOpenAICyberTranscriptLookupKeys),
  55. }
  56. nextLookupKey := 0
  57. lookupKeysRotated := false
  58. lastLookupKey := ""
  59. // This is an entropy heuristic, not provenance proof: authenticated
  60. // server-side history would be required to distinguish fixed few-shot
  61. // assistant items perfectly.
  62. hasModelGeneratedItem := false
  63. sequence.ForEach(func(_, item gjson.Result) bool {
  64. canonical := item.Raw
  65. switch item.Type {
  66. case gjson.String:
  67. encoded, _ := json.Marshal(item.String())
  68. canonical = string(encoded)
  69. case gjson.JSON:
  70. canonical = normalizeCompatSeedJSON(json.RawMessage(item.Raw))
  71. }
  72. if strings.TrimSpace(canonical) == "" {
  73. return true
  74. }
  75. if openAICyberTranscriptItemStartsUserTurn(item) && hasModelGeneratedItem && lastLookupKey != "" {
  76. result.preLatestUserKey = lastLookupKey
  77. }
  78. _, _ = h.Write([]byte("|item="))
  79. _, _ = h.Write([]byte(canonical))
  80. lastLookupKey = hex.EncodeToString(h.Sum(nil))
  81. if len(result.lookupKeys) < maxOpenAICyberTranscriptLookupKeys {
  82. result.lookupKeys = append(result.lookupKeys, lastLookupKey)
  83. } else {
  84. result.lookupKeys[nextLookupKey] = lastLookupKey
  85. nextLookupKey = (nextLookupKey + 1) % maxOpenAICyberTranscriptLookupKeys
  86. lookupKeysRotated = true
  87. result.lookupKeysTruncated = true
  88. }
  89. if openAICyberTranscriptItemIsModelGenerated(item) {
  90. hasModelGeneratedItem = true
  91. }
  92. return true
  93. })
  94. if lookupKeysRotated {
  95. ordered := make([]string, 0, len(result.lookupKeys))
  96. ordered = append(ordered, result.lookupKeys[nextLookupKey:]...)
  97. ordered = append(ordered, result.lookupKeys[:nextLookupKey]...)
  98. result.lookupKeys = ordered
  99. }
  100. return result
  101. }
  102. if messages := root.Get("messages"); messages.Exists() {
  103. return appendSequence(messages)
  104. }
  105. input := root.Get("input")
  106. if input.IsArray() {
  107. return appendSequence(input)
  108. }
  109. if input.Type == gjson.String && strings.TrimSpace(input.String()) != "" {
  110. encoded, _ := json.Marshal(input.String())
  111. _, _ = h.Write([]byte("|item="))
  112. _, _ = h.Write(encoded)
  113. return openAICyberTranscriptBlockKeys{lookupKeys: []string{hex.EncodeToString(h.Sum(nil))}}
  114. }
  115. return openAICyberTranscriptBlockKeys{}
  116. }
  117. func openAICyberTranscriptItemStartsUserTurn(item gjson.Result) bool {
  118. if strings.EqualFold(strings.TrimSpace(item.Get("role").String()), "user") {
  119. content := item.Get("content")
  120. if content.IsArray() {
  121. hasUserContent := false
  122. content.ForEach(func(_, block gjson.Result) bool {
  123. switch strings.ToLower(strings.TrimSpace(block.Get("type").String())) {
  124. case "tool_result", "function_call_output", "custom_tool_call_output", "computer_call_output":
  125. default:
  126. hasUserContent = true
  127. }
  128. return !hasUserContent
  129. })
  130. return hasUserContent
  131. }
  132. return content.Exists()
  133. }
  134. return strings.EqualFold(strings.TrimSpace(item.Get("type").String()), "input_text")
  135. }
  136. func openAICyberTranscriptItemIsModelGenerated(item gjson.Result) bool {
  137. switch strings.ToLower(strings.TrimSpace(item.Get("role").String())) {
  138. case "assistant", "model":
  139. return true
  140. }
  141. switch strings.ToLower(strings.TrimSpace(item.Get("type").String())) {
  142. case "output_text", "function_call", "tool_call", "custom_tool_call", "computer_call":
  143. return true
  144. default:
  145. return false
  146. }
  147. }