package synthesizer import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" ) // StableIDGenerator generates stable, deterministic IDs for auth entries. // It uses SHA256 hashing with collision handling via counters. // It is not safe for concurrent use. type StableIDGenerator struct { counters map[string]int } // NewStableIDGenerator creates a new StableIDGenerator instance. func NewStableIDGenerator() *StableIDGenerator { return &StableIDGenerator{counters: make(map[string]int)} } // Next generates a stable ID based on the kind and parts. // Returns the full ID (kind:hash) and the short hash portion. func (g *StableIDGenerator) Next(kind string, parts ...string) (string, string) { if g == nil { return kind + ":000000000000", "000000000000" } hasher := sha256.New() hasher.Write([]byte(kind)) for _, part := range parts { trimmed := strings.TrimSpace(part) hasher.Write([]byte{0}) hasher.Write([]byte(trimmed)) } digest := hex.EncodeToString(hasher.Sum(nil)) if len(digest) < 12 { digest = fmt.Sprintf("%012s", digest) } short := digest[:12] key := kind + ":" + short index := g.counters[key] g.counters[key] = index + 1 if index > 0 { short = fmt.Sprintf("%s-%d", short, index) } return fmt.Sprintf("%s:%s", kind, short), short } // ApplyAuthExcludedModelsMeta applies excluded models metadata to an auth entry. // It computes a hash of excluded models and sets the auth_kind attribute. func ApplyAuthExcludedModelsMeta(auth *coreauth.Auth, cfg *config.Config, perKey []string, authKind string) { if auth == nil || cfg == nil { return } authKindKey := strings.ToLower(strings.TrimSpace(authKind)) seen := make(map[string]struct{}) add := func(list []string) { for _, entry := range list { if trimmed := strings.TrimSpace(entry); trimmed != "" { key := strings.ToLower(trimmed) if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} } } } if authKindKey == "apikey" { add(perKey) } else if cfg.OAuthExcludedModels != nil { providerKey := strings.ToLower(strings.TrimSpace(auth.Provider)) add(cfg.OAuthExcludedModels[providerKey]) } combined := make([]string, 0, len(seen)) for k := range seen { combined = append(combined, k) } sort.Strings(combined) hash := diff.ComputeExcludedModelsHash(combined) if auth.Attributes == nil { auth.Attributes = make(map[string]string) } if hash != "" { auth.Attributes["excluded_models_hash"] = hash } if authKind != "" { auth.Attributes["auth_kind"] = authKind } } // addConfigHeadersToAttrs adds header configuration to auth attributes. // Headers are prefixed with "header:" in the attributes map. func addConfigHeadersToAttrs(headers map[string]string, attrs map[string]string) { if len(headers) == 0 || attrs == nil { return } for hk, hv := range headers { key := strings.TrimSpace(hk) val := strings.TrimSpace(hv) if key == "" || val == "" { continue } attrs["header:"+key] = val } }