Spaces:
Paused
Paused
| package cache | |
| import ( | |
| "crypto/sha256" | |
| "encoding/hex" | |
| "strings" | |
| "sync" | |
| "time" | |
| ) | |
| // SignatureEntry holds a cached thinking signature with timestamp | |
| type SignatureEntry struct { | |
| Signature string | |
| Timestamp time.Time | |
| } | |
| const ( | |
| // SignatureCacheTTL is how long signatures are valid | |
| SignatureCacheTTL = 3 * time.Hour | |
| // SignatureTextHashLen is the length of the hash key (16 hex chars = 64-bit key space) | |
| SignatureTextHashLen = 16 | |
| // MinValidSignatureLen is the minimum length for a signature to be considered valid | |
| MinValidSignatureLen = 50 | |
| // CacheCleanupInterval controls how often stale entries are purged | |
| CacheCleanupInterval = 10 * time.Minute | |
| ) | |
| // signatureCache stores signatures by model group -> textHash -> SignatureEntry | |
| var signatureCache sync.Map | |
| // cacheCleanupOnce ensures the background cleanup goroutine starts only once | |
| var cacheCleanupOnce sync.Once | |
| // groupCache is the inner map type | |
| type groupCache struct { | |
| mu sync.RWMutex | |
| entries map[string]SignatureEntry | |
| } | |
| // hashText creates a stable, Unicode-safe key from text content | |
| func hashText(text string) string { | |
| h := sha256.Sum256([]byte(text)) | |
| return hex.EncodeToString(h[:])[:SignatureTextHashLen] | |
| } | |
| // getOrCreateGroupCache gets or creates a cache bucket for a model group | |
| func getOrCreateGroupCache(groupKey string) *groupCache { | |
| // Start background cleanup on first access | |
| cacheCleanupOnce.Do(startCacheCleanup) | |
| if val, ok := signatureCache.Load(groupKey); ok { | |
| return val.(*groupCache) | |
| } | |
| sc := &groupCache{entries: make(map[string]SignatureEntry)} | |
| actual, _ := signatureCache.LoadOrStore(groupKey, sc) | |
| return actual.(*groupCache) | |
| } | |
| // startCacheCleanup launches a background goroutine that periodically | |
| // removes caches where all entries have expired. | |
| func startCacheCleanup() { | |
| go func() { | |
| ticker := time.NewTicker(CacheCleanupInterval) | |
| defer ticker.Stop() | |
| for range ticker.C { | |
| purgeExpiredCaches() | |
| } | |
| }() | |
| } | |
| // purgeExpiredCaches removes caches with no valid (non-expired) entries. | |
| func purgeExpiredCaches() { | |
| now := time.Now() | |
| signatureCache.Range(func(key, value any) bool { | |
| sc := value.(*groupCache) | |
| sc.mu.Lock() | |
| // Remove expired entries | |
| for k, entry := range sc.entries { | |
| if now.Sub(entry.Timestamp) > SignatureCacheTTL { | |
| delete(sc.entries, k) | |
| } | |
| } | |
| isEmpty := len(sc.entries) == 0 | |
| sc.mu.Unlock() | |
| // Remove cache bucket if empty | |
| if isEmpty { | |
| signatureCache.Delete(key) | |
| } | |
| return true | |
| }) | |
| } | |
| // CacheSignature stores a thinking signature for a given model group and text. | |
| // Used for Claude models that require signed thinking blocks in multi-turn conversations. | |
| func CacheSignature(modelName, text, signature string) { | |
| if text == "" || signature == "" { | |
| return | |
| } | |
| if len(signature) < MinValidSignatureLen { | |
| return | |
| } | |
| groupKey := GetModelGroup(modelName) | |
| textHash := hashText(text) | |
| sc := getOrCreateGroupCache(groupKey) | |
| sc.mu.Lock() | |
| defer sc.mu.Unlock() | |
| sc.entries[textHash] = SignatureEntry{ | |
| Signature: signature, | |
| Timestamp: time.Now(), | |
| } | |
| } | |
| // GetCachedSignature retrieves a cached signature for a given model group and text. | |
| // Returns empty string if not found or expired. | |
| func GetCachedSignature(modelName, text string) string { | |
| groupKey := GetModelGroup(modelName) | |
| if text == "" { | |
| if groupKey == "gemini" { | |
| return "skip_thought_signature_validator" | |
| } | |
| return "" | |
| } | |
| val, ok := signatureCache.Load(groupKey) | |
| if !ok { | |
| if groupKey == "gemini" { | |
| return "skip_thought_signature_validator" | |
| } | |
| return "" | |
| } | |
| sc := val.(*groupCache) | |
| textHash := hashText(text) | |
| now := time.Now() | |
| sc.mu.Lock() | |
| entry, exists := sc.entries[textHash] | |
| if !exists { | |
| sc.mu.Unlock() | |
| if groupKey == "gemini" { | |
| return "skip_thought_signature_validator" | |
| } | |
| return "" | |
| } | |
| if now.Sub(entry.Timestamp) > SignatureCacheTTL { | |
| delete(sc.entries, textHash) | |
| sc.mu.Unlock() | |
| if groupKey == "gemini" { | |
| return "skip_thought_signature_validator" | |
| } | |
| return "" | |
| } | |
| // Refresh TTL on access (sliding expiration). | |
| entry.Timestamp = now | |
| sc.entries[textHash] = entry | |
| sc.mu.Unlock() | |
| return entry.Signature | |
| } | |
| // ClearSignatureCache clears signature cache for a specific model group or all groups. | |
| func ClearSignatureCache(modelName string) { | |
| if modelName == "" { | |
| signatureCache.Range(func(key, _ any) bool { | |
| signatureCache.Delete(key) | |
| return true | |
| }) | |
| return | |
| } | |
| groupKey := GetModelGroup(modelName) | |
| signatureCache.Delete(groupKey) | |
| } | |
| // HasValidSignature checks if a signature is valid (non-empty and long enough) | |
| func HasValidSignature(modelName, signature string) bool { | |
| return (signature != "" && len(signature) >= MinValidSignatureLen) || (signature == "skip_thought_signature_validator" && GetModelGroup(modelName) == "gemini") | |
| } | |
| func GetModelGroup(modelName string) string { | |
| if strings.Contains(modelName, "gpt") { | |
| return "gpt" | |
| } else if strings.Contains(modelName, "claude") { | |
| return "claude" | |
| } else if strings.Contains(modelName, "gemini") { | |
| return "gemini" | |
| } | |
| return modelName | |
| } | |