package model import ( "crypto/sha256" "errors" "fmt" "strings" "sync" "github.com/QuantumNous/new-api/common" "gorm.io/gorm" "gorm.io/gorm/clause" ) var metadataMutationMu sync.Mutex // metadataTransaction serializes changes to vendors and model ownership before // acquiring model/vendor row locks. The option anchor also covers empty tables. func metadataTransaction(change func(*gorm.DB) error) error { metadataMutationMu.Lock() defer metadataMutationMu.Unlock() return DB.Transaction(func(tx *gorm.DB) error { if err := lockMetadataMutation(tx); err != nil { return err } return change(tx) }) } func lockMetadataMutation(tx *gorm.DB) error { anchor := Option{Key: "metadata_sync_lock", Value: ""} if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&anchor).Error; err != nil { return err } return lockForUpdate(tx).Where(commonKeyCol+" = ?", anchor.Key).First(&anchor).Error } var ErrMetadataSyncConflict = errors.New("metadata changed; preview again before applying") var MetadataSyncFields = []string{"description", "icon", "tags", "vendor", "endpoints", "name_rule", "status"} type MetadataValues struct { Description string `json:"description"` Icon string `json:"icon"` Tags string `json:"tags"` Vendor string `json:"vendor"` Endpoints string `json:"endpoints"` NameRule int `json:"name_rule"` Status int `json:"status"` } type MetadataSyncSelection struct { ModelName string `json:"model_name"` RecordVersion string `json:"record_version"` Create bool `json:"create"` Fields []string `json:"fields"` } type MetadataSyncUpdate struct { MetadataSyncSelection Values MetadataValues } type MetadataSyncResult struct { CreatedModels []string `json:"created_models"` UpdatedModels []MetadataSyncSelection `json:"updated_models"` CreatedVendors []string `json:"created_vendors"` } func MetadataRecordVersion(local *Model, localVendor, upstreamVendor *Vendor) string { encoded, _ := common.Marshal([]any{local, localVendor, upstreamVendor}) return fmt.Sprintf("%x", sha256.Sum256(encoded)) } func GetMetadataSyncState(db *gorm.DB) (map[string]*Model, map[string]*Vendor, error) { var models []*Model var vendors []*Vendor if err := db.Session(&gorm.Session{}).Order("id").Find(&models).Error; err != nil { return nil, nil, err } if err := db.Session(&gorm.Session{}).Order("id").Find(&vendors).Error; err != nil { return nil, nil, err } modelMap := make(map[string]*Model, len(models)) vendorMap := make(map[string]*Vendor, len(vendors)) for _, item := range models { modelMap[item.ModelName] = item } for _, item := range vendors { vendorMap[item.Name] = item } return modelMap, vendorMap, nil } func ValidateMetadataValues(values MetadataValues) error { if values.NameRule < NameRuleExact || values.NameRule > NameRuleSuffix { return errors.New("invalid metadata matching rule") } if values.Status != 0 && values.Status != 1 { return errors.New("invalid catalog visibility") } return ValidateModelEndpoints(values.Endpoints) } // ValidateModelEndpoints accepts the existing map form (custom paths) and // type-array form (declared protocols), but never arbitrary JSON scalars. func ValidateModelEndpoints(raw string) error { if strings.TrimSpace(raw) == "" { return nil } var value any if err := common.UnmarshalJsonStr(raw, &value); err != nil { return fmt.Errorf("invalid endpoints: %w", err) } switch endpoints := value.(type) { case []any: for _, endpoint := range endpoints { if text, ok := endpoint.(string); !ok || strings.TrimSpace(text) == "" { return errors.New("endpoint types must be non-empty strings") } } case map[string]any: for key, endpoint := range endpoints { if strings.TrimSpace(key) == "" { return errors.New("endpoint type is required") } switch details := endpoint.(type) { case string: if !strings.HasPrefix(details, "/") { return errors.New("endpoint paths must start with /") } case map[string]any: path, _ := details["path"].(string) if !strings.HasPrefix(path, "/") { return errors.New("endpoint paths must start with /") } if method, exists := details["method"]; exists { switch method { case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS": default: return errors.New("invalid endpoint HTTP method") } } default: return errors.New("endpoint configuration must be a path or object") } } default: return errors.New("endpoints must be a JSON object or array") } return nil } func ApplyMetadataSync(updates []MetadataSyncUpdate, upstreamVendors map[string]Vendor) (*MetadataSyncResult, error) { if len(updates) == 0 { return nil, errors.New("select metadata changes before applying") } seen := make(map[string]bool) for _, update := range updates { if seen[update.ModelName] || strings.TrimSpace(update.ModelName) == "" { return nil, errors.New("invalid or duplicate model selection") } seen[update.ModelName] = true if update.RecordVersion == "" { return nil, ErrMetadataSyncConflict } if err := ValidateMetadataValues(update.Values); err != nil { return nil, err } if !update.Create && len(update.Fields) == 0 { return nil, errors.New("select fields to update") } allowed := make(map[string]bool) for _, field := range MetadataSyncFields { allowed[field] = true } for _, field := range update.Fields { if !allowed[field] { return nil, fmt.Errorf("unsupported metadata field: %s", field) } } } result := &MetadataSyncResult{CreatedModels: []string{}, UpdatedModels: []MetadataSyncSelection{}, CreatedVendors: []string{}} err := metadataTransaction(func(tx *gorm.DB) error { locals, vendors, err := GetMetadataSyncState(lockForUpdate(tx)) if err != nil { return err } vendorByID := make(map[int]*Vendor) for _, vendor := range vendors { vendorByID[vendor.Id] = vendor } // Verify the entire selection before any write, including shared vendors. for _, update := range updates { local := locals[update.ModelName] if local != nil && local.SyncOfficial == 0 { return fmt.Errorf("metadata sync is disabled for %s", update.ModelName) } var localVendor *Vendor if local != nil { localVendor = vendorByID[local.VendorID] } if MetadataRecordVersion(local, localVendor, FindMetadataVendor(vendors, update.Values.Vendor)) != update.RecordVersion { return fmt.Errorf("%w: %s", ErrMetadataSyncConflict, update.ModelName) } if update.Create && local != nil || !update.Create && local == nil { return ErrMetadataSyncConflict } } for _, update := range updates { fields := make(map[string]any) selected := update.Fields if update.Create { selected = MetadataSyncFields } for _, field := range selected { switch field { case "description": fields[field] = update.Values.Description case "icon": fields[field] = update.Values.Icon case "tags": fields[field] = update.Values.Tags case "endpoints": fields[field] = update.Values.Endpoints case "name_rule": fields[field] = update.Values.NameRule case "status": fields[field] = update.Values.Status case "vendor": vendorID := 0 name := update.Values.Vendor if name != "" { vendor := FindMetadataVendor(vendors, name) if vendor == nil { up, ok := upstreamVendors[name] if !ok { return fmt.Errorf("upstream vendor not found: %s", name) } vendor = &Vendor{Name: name, Description: up.Description, Icon: up.Icon, Status: 1, CreatedTime: common.GetTimestamp(), UpdatedTime: common.GetTimestamp()} if err := validateVendorMetadata(tx, vendor); err != nil { return err } if err := tx.Create(vendor).Error; err != nil { return err } vendors[name] = vendor result.CreatedVendors = append(result.CreatedVendors, name) } vendorID = vendor.Id } fields["vendor_id"] = vendorID } } fields["updated_time"] = common.GetTimestamp() if update.Create { fields["model_name"] = update.ModelName fields["sync_official"] = 1 fields["created_time"] = common.GetTimestamp() if err := tx.Model(&Model{}).Create(fields).Error; err != nil { return err } result.CreatedModels = append(result.CreatedModels, update.ModelName) } else { if err := tx.Model(&Model{}).Where("id = ?", locals[update.ModelName].Id).Updates(fields).Error; err != nil { return err } result.UpdatedModels = append(result.UpdatedModels, update.MetadataSyncSelection) } } return nil }) if err != nil { return nil, err } RefreshPricing() return result, nil }