89 lines
2.2 KiB
Go
89 lines
2.2 KiB
Go
// Package fingerprint identifies a chat-completions conversation without a
|
|
// session id: the system prompt and the first user message never change from
|
|
// turn to turn, so hashing them pins the conversation.
|
|
package fingerprint
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"strings"
|
|
)
|
|
|
|
// maxPart is the number of bytes of each input string that contributes to the
|
|
// key: 4 KiB keeps a huge first message from slowing every turn.
|
|
const maxPart = 4096
|
|
|
|
// Of returns the lowercase hex SHA-256 of the system prompt and the first user
|
|
// message of a chat-completions body (first 4 KiB of each, joined with "\n"),
|
|
// or "" when the body is not a JSON object with a "messages" array containing a
|
|
// user message.
|
|
func Of(body []byte) string {
|
|
var doc struct {
|
|
Messages []json.RawMessage `json:"messages"`
|
|
}
|
|
if err := json.Unmarshal(body, &doc); err != nil || doc.Messages == nil {
|
|
return ""
|
|
}
|
|
|
|
var system, user string
|
|
seenSystem, seenUser := false, false
|
|
for _, raw := range doc.Messages {
|
|
var msg struct {
|
|
Role string `json:"role"`
|
|
Content json.RawMessage `json:"content"`
|
|
}
|
|
if err := json.Unmarshal(raw, &msg); err != nil {
|
|
return ""
|
|
}
|
|
switch msg.Role {
|
|
case "system":
|
|
if !seenSystem {
|
|
seenSystem = true
|
|
system = contentText(msg.Content)
|
|
}
|
|
case "user":
|
|
if !seenUser {
|
|
seenUser = true
|
|
user = contentText(msg.Content)
|
|
}
|
|
}
|
|
}
|
|
if !seenUser {
|
|
return ""
|
|
}
|
|
|
|
if len(system) > maxPart {
|
|
system = system[:maxPart]
|
|
}
|
|
if len(user) > maxPart {
|
|
user = user[:maxPart]
|
|
}
|
|
sum := sha256.Sum256([]byte(system + "\n" + user))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// contentText renders a message content value: a JSON string is returned as-is,
|
|
// an array of parts is the concatenation of its text parts (other types
|
|
// ignored), and anything else is "".
|
|
func contentText(raw json.RawMessage) string {
|
|
var text string
|
|
if err := json.Unmarshal(raw, &text); err == nil {
|
|
return text
|
|
}
|
|
var parts []struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
}
|
|
if err := json.Unmarshal(raw, &parts); err != nil {
|
|
return ""
|
|
}
|
|
var b strings.Builder
|
|
for _, p := range parts {
|
|
if p.Type == "text" {
|
|
b.WriteString(p.Text)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|