claude-code-proxy/proxy/internal/handler/utils.go

266 lines
8.3 KiB
Go
Raw Normal View History

2025-06-29 19:27:00 -04:00
package handler
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/seifghazi/claude-code-monitor/internal/model"
)
// SanitizeHeaders removes sensitive headers before logging/storage
func SanitizeHeaders(headers http.Header) http.Header {
sanitized := make(http.Header)
sensitiveHeaders := []string{
"x-api-key",
"api-key",
"authorization",
"anthropic-api-key",
"openai-api-key",
"bearer",
}
for key, values := range headers {
lowerKey := strings.ToLower(key)
isSensitive := false
for _, sensitive := range sensitiveHeaders {
if strings.Contains(lowerKey, sensitive) {
isSensitive = true
break
}
}
if isSensitive {
sanitized[key] = []string{"[REDACTED]"}
} else {
sanitized[key] = values
}
}
return sanitized
}
// ConversationDiffAnalyzer analyzes conversation flows to identify new vs repeated content
type ConversationDiffAnalyzer struct{}
// NewConversationDiffAnalyzer creates a new conversation diff analyzer
func NewConversationDiffAnalyzer() *ConversationDiffAnalyzer {
return &ConversationDiffAnalyzer{}
}
// ConversationFlowData represents the flow analysis of a conversation
type ConversationFlowData struct {
TotalMessages int `json:"totalMessages"`
NewMessages []int `json:"newMessages"` // Indices of new messages
DuplicateMessages []int `json:"duplicateMessages"` // Indices of duplicate messages
MessageHashes []string `json:"messageHashes"` // Content hashes for deduplication
ConversationHash string `json:"conversationHash"` // Hash of entire conversation
PreviousHash string `json:"previousHash"` // Hash of previous conversation state
Changes []ConversationChange `json:"changes"` // Detailed changes
FlowMetadata map[string]interface{} `json:"flowMetadata"` // Additional metadata
}
// ConversationChange represents a specific change in the conversation
type ConversationChange struct {
Type string `json:"type"` // "added", "modified", "context"
MessageIdx int `json:"messageIdx"` // Index of the message
Role string `json:"role"` // Role of the message
ContentHash string `json:"contentHash"` // Hash of the content
Preview string `json:"preview"` // Short preview of content
Timestamp string `json:"timestamp"` // When this change was detected
}
// AnalyzeConversationFlow analyzes a conversation to identify what's new vs repeated
func (c *ConversationDiffAnalyzer) AnalyzeConversationFlow(messages []model.AnthropicMessage, previousConversation []model.AnthropicMessage) *ConversationFlowData {
totalMessages := len(messages)
// Create hashes for current conversation
currentHashes := make([]string, totalMessages)
for i, msg := range messages {
currentHashes[i] = c.hashMessage(msg)
}
// Create hashes for previous conversation (if any)
var previousHashes []string
if previousConversation != nil {
previousHashes = make([]string, len(previousConversation))
for i, msg := range previousConversation {
previousHashes[i] = c.hashMessage(msg)
}
}
// Identify new vs duplicate messages
newMessages := []int{}
duplicateMessages := []int{}
changes := []ConversationChange{}
// Simple approach: messages that appear after the previous conversation length are new
previousLength := len(previousHashes)
for i, msg := range messages {
isNew := i >= previousLength
// More sophisticated check: compare hashes
if !isNew && i < len(previousHashes) {
isNew = currentHashes[i] != previousHashes[i]
}
if isNew {
newMessages = append(newMessages, i)
changes = append(changes, ConversationChange{
Type: "added",
MessageIdx: i,
Role: msg.Role,
ContentHash: currentHashes[i],
Preview: c.getMessagePreview(msg),
Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
})
} else {
duplicateMessages = append(duplicateMessages, i)
changes = append(changes, ConversationChange{
Type: "context",
MessageIdx: i,
Role: msg.Role,
ContentHash: currentHashes[i],
Preview: c.getMessagePreview(msg),
Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
})
}
}
// If no previous conversation, consider a reasonable threshold of "new" vs "context"
if previousConversation == nil && totalMessages > 1 {
// Heuristic: last 30% of messages are "new", rest is context
newThreshold := max(1, int(float64(totalMessages)*0.3))
contextEnd := totalMessages - newThreshold
newMessages = []int{}
duplicateMessages = []int{}
changes = []ConversationChange{}
for i := 0; i < totalMessages; i++ {
if i >= contextEnd {
newMessages = append(newMessages, i)
changes = append(changes, ConversationChange{
Type: "added",
MessageIdx: i,
Role: messages[i].Role,
ContentHash: currentHashes[i],
Preview: c.getMessagePreview(messages[i]),
Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
})
} else {
duplicateMessages = append(duplicateMessages, i)
changes = append(changes, ConversationChange{
Type: "context",
MessageIdx: i,
Role: messages[i].Role,
ContentHash: currentHashes[i],
Preview: c.getMessagePreview(messages[i]),
Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
})
}
}
}
// Generate conversation hashes
conversationHash := c.hashConversation(messages)
previousHash := ""
if previousConversation != nil {
previousHash = c.hashConversation(previousConversation)
}
return &ConversationFlowData{
TotalMessages: totalMessages,
NewMessages: newMessages,
DuplicateMessages: duplicateMessages,
MessageHashes: currentHashes,
ConversationHash: conversationHash,
PreviousHash: previousHash,
Changes: changes,
FlowMetadata: map[string]interface{}{
"newCount": len(newMessages),
"duplicateCount": len(duplicateMessages),
"analyzeTime": time.Now().Format(time.RFC3339),
},
}
}
// hashMessage creates a hash of a message for deduplication
func (c *ConversationDiffAnalyzer) hashMessage(msg model.AnthropicMessage) string {
// Create a stable representation of the message
content := c.normalizeMessageContent(msg.Content)
data := fmt.Sprintf("%s|%s", msg.Role, content)
hash := sha256.Sum256([]byte(data))
return fmt.Sprintf("%x", hash[:8]) // Use first 8 bytes for shorter hash
}
// hashConversation creates a hash of the entire conversation
func (c *ConversationDiffAnalyzer) hashConversation(messages []model.AnthropicMessage) string {
var parts []string
for _, msg := range messages {
parts = append(parts, c.hashMessage(msg))
}
conversationData := strings.Join(parts, "|")
hash := sha256.Sum256([]byte(conversationData))
return fmt.Sprintf("%x", hash[:16]) // Use first 16 bytes for conversation hash
}
// normalizeMessageContent converts message content to a normalized string
func (c *ConversationDiffAnalyzer) normalizeMessageContent(content interface{}) string {
switch v := content.(type) {
case string:
return strings.TrimSpace(v)
case []interface{}:
var parts []string
for _, item := range v {
if block, ok := item.(map[string]interface{}); ok {
if text, hasText := block["text"].(string); hasText {
parts = append(parts, strings.TrimSpace(text))
} else if blockType, hasType := block["type"].(string); hasType {
// Handle different content types (tool_use, etc.)
switch blockType {
case "tool_use":
if name, hasName := block["name"].(string); hasName {
parts = append(parts, fmt.Sprintf("TOOL:%s", name))
}
case "tool_result":
parts = append(parts, "TOOL_RESULT")
default:
parts = append(parts, fmt.Sprintf("CONTENT:%s", blockType))
}
}
}
}
return strings.Join(parts, " ")
default:
// Convert to JSON and back for normalization
jsonBytes, _ := json.Marshal(content)
return string(jsonBytes)
}
}
// getMessagePreview creates a short preview of a message
func (c *ConversationDiffAnalyzer) getMessagePreview(msg model.AnthropicMessage) string {
content := c.normalizeMessageContent(msg.Content)
if len(content) > 100 {
return content[:100] + "..."
}
return content
}
// max returns the maximum of two integers
func max(a, b int) int {
if a > b {
return a
}
return b
}