| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- 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
- }
- }
|