mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 04:10:51 +08:00
Addresses an issue where thinking signature validation fails due to model mapping and empty internal registry. - Implements a fallback mechanism in the router to use the global model registry when the internal registry is empty. This ensures that models registered via API keys are correctly resolved even without local provider configurations. - Modifies `GetModelGroup` to use registry-based grouping in addition to name pattern matching, covering cases where models are registered with API keys but lack provider names in their names. - Updates signature validation to compare model groups instead of exact model names. These changes resolve thinking signature validation errors and improve the accuracy of model resolution.
215 lines
5.5 KiB
Go
215 lines
5.5 KiB
Go
package cache
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
)
|
|
|
|
// 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 {
|
|
// Fast path: check model name patterns first
|
|
if strings.Contains(modelName, "gpt") {
|
|
return "gpt"
|
|
} else if strings.Contains(modelName, "claude") {
|
|
return "claude"
|
|
} else if strings.Contains(modelName, "gemini") {
|
|
return "gemini"
|
|
}
|
|
|
|
// Slow path: check registry for provider-based grouping
|
|
// This handles models registered via claude-api-key, gemini-api-key, etc.
|
|
// that don't have provider name in their model name (e.g., kimi-k2.5 via claude-api-key)
|
|
if providers := registry.GetGlobalRegistry().GetModelProviders(modelName); len(providers) > 0 {
|
|
provider := strings.ToLower(providers[0])
|
|
switch provider {
|
|
case "claude":
|
|
return "claude"
|
|
case "gemini", "gemini-cli", "aistudio", "vertex", "antigravity":
|
|
return "gemini"
|
|
case "codex":
|
|
return "gpt"
|
|
}
|
|
}
|
|
|
|
return modelName
|
|
}
|