Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
342
backend/internal/cache/signature_cache.go
vendored
Normal file
342
backend/internal/cache/signature_cache.go
vendored
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
type signatureKVClient interface {
|
||||
KVGet(ctx context.Context, key string) ([]byte, bool, error)
|
||||
KVSet(ctx context.Context, key string, value []byte, opts homekv.KVSetOptions) (bool, error)
|
||||
KVDel(ctx context.Context, keys ...string) (int64, error)
|
||||
KVExpire(ctx context.Context, key string, ttl time.Duration) (bool, error)
|
||||
}
|
||||
|
||||
var currentSignatureKVClient = func() (signatureKVClient, bool, error) {
|
||||
return homekv.CurrentKVClient()
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
purgeExpiredCodexReasoningReplayCache(now)
|
||||
purgeExpiredXAIReasoningReplayCache(now)
|
||||
purgeExpiredAntigravityReasoningReplayCache(now)
|
||||
purgeExpiredKimiThinkingReplayCache(now)
|
||||
purgeExpiredClaudeThinkingReplayCache(now)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
CacheSignatureBestEffort(context.Background(), modelName, text, signature)
|
||||
}
|
||||
|
||||
// CacheSignatureBestEffort stores a thinking signature for completed response paths.
|
||||
func CacheSignatureBestEffort(ctx context.Context, modelName, text, signature string) bool {
|
||||
if text == "" || signature == "" {
|
||||
return false
|
||||
}
|
||||
if len(signature) < MinValidSignatureLen {
|
||||
return false
|
||||
}
|
||||
|
||||
if client, homeMode, errClient := currentSignatureKVClient(); homeMode {
|
||||
if errClient != nil {
|
||||
log.Errorf("home kv best-effort signature set failed prefix=cpa:signature:*: %v", errClient)
|
||||
return false
|
||||
}
|
||||
written, errSet := client.KVSet(ctx, signatureKVKey(modelName, text), []byte(signature), homekv.KVSetOptions{EX: SignatureCacheTTL})
|
||||
if errSet != nil {
|
||||
log.Errorf("home kv best-effort signature set failed prefix=cpa:signature:*: %v", errSet)
|
||||
return false
|
||||
}
|
||||
return written
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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 {
|
||||
signature, errSignature := GetCachedSignatureRequired(context.Background(), modelName, text)
|
||||
if errSignature != nil {
|
||||
return ""
|
||||
}
|
||||
return signature
|
||||
}
|
||||
|
||||
// GetCachedSignatureRequired retrieves a cached signature for request-time paths.
|
||||
func GetCachedSignatureRequired(ctx context.Context, modelName, text string) (string, error) {
|
||||
groupKey := GetModelGroup(modelName)
|
||||
|
||||
if text == "" {
|
||||
if groupKey == "gemini" {
|
||||
return "skip_thought_signature_validator", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if client, homeMode, errClient := currentSignatureKVClient(); homeMode {
|
||||
if errClient != nil {
|
||||
return "", errClient
|
||||
}
|
||||
key := signatureKVKey(modelName, text)
|
||||
raw, found, errGet := client.KVGet(ctx, key)
|
||||
if errGet != nil {
|
||||
return "", errGet
|
||||
}
|
||||
if !found {
|
||||
if groupKey == "gemini" {
|
||||
return "skip_thought_signature_validator", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
if _, errExpire := client.KVExpire(ctx, key, SignatureCacheTTL); errExpire != nil {
|
||||
return "", errExpire
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
val, ok := signatureCache.Load(groupKey)
|
||||
if !ok {
|
||||
if groupKey == "gemini" {
|
||||
return "skip_thought_signature_validator", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
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", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
if now.Sub(entry.Timestamp) > SignatureCacheTTL {
|
||||
delete(sc.entries, textHash)
|
||||
sc.mu.Unlock()
|
||||
if groupKey == "gemini" {
|
||||
return "skip_thought_signature_validator", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Refresh TTL on access (sliding expiration).
|
||||
entry.Timestamp = now
|
||||
sc.entries[textHash] = entry
|
||||
sc.mu.Unlock()
|
||||
|
||||
return entry.Signature, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// DeleteCachedSignatureRequired removes one exact cached signature.
|
||||
func DeleteCachedSignatureRequired(ctx context.Context, modelName, text string) error {
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
if client, homeMode, errClient := currentSignatureKVClient(); homeMode {
|
||||
if errClient != nil {
|
||||
return errClient
|
||||
}
|
||||
_, errDel := client.KVDel(ctx, signatureKVKey(modelName, text))
|
||||
return errDel
|
||||
}
|
||||
groupKey := GetModelGroup(modelName)
|
||||
textHash := hashText(text)
|
||||
val, ok := signatureCache.Load(groupKey)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
sc := val.(*groupCache)
|
||||
sc.mu.Lock()
|
||||
delete(sc.entries, textHash)
|
||||
isEmpty := len(sc.entries) == 0
|
||||
sc.mu.Unlock()
|
||||
if isEmpty {
|
||||
signatureCache.Delete(groupKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func signatureKVKey(modelName, text string) string {
|
||||
return fmt.Sprintf("cpa:signature:%s:%s", GetModelGroup(modelName), homekv.HashKeyPart(text))
|
||||
}
|
||||
|
||||
var signatureCacheEnabled atomic.Bool
|
||||
var signatureBypassStrictMode atomic.Bool
|
||||
|
||||
func init() {
|
||||
signatureCacheEnabled.Store(true)
|
||||
signatureBypassStrictMode.Store(false)
|
||||
}
|
||||
|
||||
// SetSignatureCacheEnabled switches Antigravity signature handling between cache mode and bypass mode.
|
||||
func SetSignatureCacheEnabled(enabled bool) {
|
||||
previous := signatureCacheEnabled.Swap(enabled)
|
||||
if previous == enabled {
|
||||
return
|
||||
}
|
||||
if !enabled {
|
||||
log.Info("antigravity signature cache DISABLED - bypass mode active, cached signatures will not be used for request translation")
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureCacheEnabled returns whether signature cache validation is enabled.
|
||||
func SignatureCacheEnabled() bool {
|
||||
return signatureCacheEnabled.Load()
|
||||
}
|
||||
|
||||
// SetSignatureBypassStrictMode controls whether bypass mode uses strict protobuf-tree validation.
|
||||
func SetSignatureBypassStrictMode(strict bool) {
|
||||
previous := signatureBypassStrictMode.Swap(strict)
|
||||
if previous == strict {
|
||||
return
|
||||
}
|
||||
if strict {
|
||||
log.Debug("antigravity bypass signature validation: strict mode (protobuf tree)")
|
||||
} else {
|
||||
log.Debug("antigravity bypass signature validation: basic mode (R/E + 0x12)")
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureBypassStrictMode returns whether bypass mode uses strict protobuf-tree validation.
|
||||
func SignatureBypassStrictMode() bool {
|
||||
return signatureBypassStrictMode.Load()
|
||||
}
|
||||
Loading…
Reference in a new issue