// 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() }