Files
sub2api/backend/internal/service/openai_cyber_transcript.go
T

158 lines
5.2 KiB
Go

package service
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"strconv"
"strings"
"github.com/tidwall/gjson"
)
type openAICyberTranscriptBlockKeys struct {
lookupKeys []string
preLatestUserKey string
lookupKeysTruncated bool
}
// Bound the Redis lookup work for a single request while retaining the most
// recent transcript prefixes, where a continuation is most likely to match.
const maxOpenAICyberTranscriptLookupKeys = 256
// deriveOpenAICyberTranscriptBlockKeys returns cumulative semantic-history
// hashes plus the context key immediately before the latest user turn. The
// context key requires model-generated history so shared first-turn templates
// cannot block unrelated conversations.
func deriveOpenAICyberTranscriptBlockKeys(apiKeyID int64, body []byte) openAICyberTranscriptBlockKeys {
if len(body) == 0 {
return openAICyberTranscriptBlockKeys{}
}
root := openAIRequestPayloadView(body)
if !root.Exists() || !root.IsObject() {
return openAICyberTranscriptBlockKeys{}
}
h := sha256.New()
_, _ = h.Write([]byte("cyber-transcript:v3|api_key="))
_, _ = h.Write([]byte(strconv.FormatInt(apiKeyID, 10)))
// Model and tool definitions are request configuration rather than history.
// Root instructions remain model-visible context and participate in identity.
for _, field := range []string{"instructions"} {
v := root.Get(field)
if !v.Exists() || (v.Type == gjson.String && strings.TrimSpace(v.String()) == "") {
continue
}
canonical := normalizeCompatSeedJSON(json.RawMessage(v.Raw))
if v.Type == gjson.String {
canonical = v.String()
}
_, _ = h.Write([]byte("|"))
_, _ = h.Write([]byte(field))
_, _ = h.Write([]byte("="))
_, _ = h.Write([]byte(canonical))
}
appendSequence := func(sequence gjson.Result) openAICyberTranscriptBlockKeys {
if !sequence.Exists() || !sequence.IsArray() {
return openAICyberTranscriptBlockKeys{}
}
result := openAICyberTranscriptBlockKeys{
lookupKeys: make([]string, 0, maxOpenAICyberTranscriptLookupKeys),
}
nextLookupKey := 0
lookupKeysRotated := false
lastLookupKey := ""
// This is an entropy heuristic, not provenance proof: authenticated
// server-side history would be required to distinguish fixed few-shot
// assistant items perfectly.
hasModelGeneratedItem := false
sequence.ForEach(func(_, item gjson.Result) bool {
canonical := item.Raw
switch item.Type {
case gjson.String:
encoded, _ := json.Marshal(item.String())
canonical = string(encoded)
case gjson.JSON:
canonical = normalizeCompatSeedJSON(json.RawMessage(item.Raw))
}
if strings.TrimSpace(canonical) == "" {
return true
}
if openAICyberTranscriptItemStartsUserTurn(item) && hasModelGeneratedItem && lastLookupKey != "" {
result.preLatestUserKey = lastLookupKey
}
_, _ = h.Write([]byte("|item="))
_, _ = h.Write([]byte(canonical))
lastLookupKey = hex.EncodeToString(h.Sum(nil))
if len(result.lookupKeys) < maxOpenAICyberTranscriptLookupKeys {
result.lookupKeys = append(result.lookupKeys, lastLookupKey)
} else {
result.lookupKeys[nextLookupKey] = lastLookupKey
nextLookupKey = (nextLookupKey + 1) % maxOpenAICyberTranscriptLookupKeys
lookupKeysRotated = true
result.lookupKeysTruncated = true
}
if openAICyberTranscriptItemIsModelGenerated(item) {
hasModelGeneratedItem = true
}
return true
})
if lookupKeysRotated {
ordered := make([]string, 0, len(result.lookupKeys))
ordered = append(ordered, result.lookupKeys[nextLookupKey:]...)
ordered = append(ordered, result.lookupKeys[:nextLookupKey]...)
result.lookupKeys = ordered
}
return result
}
if messages := root.Get("messages"); messages.Exists() {
return appendSequence(messages)
}
input := root.Get("input")
if input.IsArray() {
return appendSequence(input)
}
if input.Type == gjson.String && strings.TrimSpace(input.String()) != "" {
encoded, _ := json.Marshal(input.String())
_, _ = h.Write([]byte("|item="))
_, _ = h.Write(encoded)
return openAICyberTranscriptBlockKeys{lookupKeys: []string{hex.EncodeToString(h.Sum(nil))}}
}
return openAICyberTranscriptBlockKeys{}
}
func openAICyberTranscriptItemStartsUserTurn(item gjson.Result) bool {
if strings.EqualFold(strings.TrimSpace(item.Get("role").String()), "user") {
content := item.Get("content")
if content.IsArray() {
hasUserContent := false
content.ForEach(func(_, block gjson.Result) bool {
switch strings.ToLower(strings.TrimSpace(block.Get("type").String())) {
case "tool_result", "function_call_output", "custom_tool_call_output", "computer_call_output":
default:
hasUserContent = true
}
return !hasUserContent
})
return hasUserContent
}
return content.Exists()
}
return strings.EqualFold(strings.TrimSpace(item.Get("type").String()), "input_text")
}
func openAICyberTranscriptItemIsModelGenerated(item gjson.Result) bool {
switch strings.ToLower(strings.TrimSpace(item.Get("role").String())) {
case "assistant", "model":
return true
}
switch strings.ToLower(strings.TrimSpace(item.Get("type").String())) {
case "output_text", "function_call", "tool_call", "custom_tool_call", "computer_call":
return true
default:
return false
}
}