package service import ( "context" "crypto/sha256" "errors" "fmt" "strconv" "strings" "time" ) const ( maxGroupNameRunes = 100 duplicateGroupInactiveStatus = "inactive" ) func duplicateGroupOperationID(sourceID int64, actorScope, operationKey string) string { operationKey = strings.TrimSpace(operationKey) if operationKey == "" { return "" } actorScope = strings.TrimSpace(actorScope) if actorScope == "" { actorScope = "admin:0" } payload := "admin.groups.duplicate\x00" + actorScope + "\x00" + strconv.FormatInt(sourceID, 10) + "\x00" + operationKey digest := sha256.Sum256([]byte(payload)) return fmt.Sprintf("%x", digest) } func duplicateGroupName(sourceName string, copyNumber int) string { if copyNumber < 1 { copyNumber = 1 } suffix := " (Copy)" if copyNumber > 1 { suffix = fmt.Sprintf(" (Copy %d)", copyNumber) } baseRunes := []rune(strings.TrimSpace(sourceName)) maxBaseRunes := maxGroupNameRunes - len([]rune(suffix)) if maxBaseRunes < 0 { maxBaseRunes = 0 } if len(baseRunes) > maxBaseRunes { baseRunes = baseRunes[:maxBaseRunes] } return string(baseRunes) + suffix } func cloneGroupValuePointer[T any](value *T) *T { if value == nil { return nil } cloned := *value return &cloned } func cloneGroupModelRouting(value map[string][]int64) map[string][]int64 { if value == nil { return nil } cloned := make(map[string][]int64, len(value)) for model, accountIDs := range value { cloned[model] = append([]int64(nil), accountIDs...) } return cloned } func cloneGroupVideoModelPrices(value map[string]map[string]float64) map[string]map[string]float64 { if value == nil { return nil } cloned := make(map[string]map[string]float64, len(value)) for model, prices := range value { clonedPrices := make(map[string]float64, len(prices)) for resolution, price := range prices { clonedPrices[resolution] = price } cloned[model] = clonedPrices } return cloned } func cloneGroupMessagesDispatchModelConfig(value OpenAIMessagesDispatchModelConfig) OpenAIMessagesDispatchModelConfig { cloned := value if value.ExactModelMappings != nil { cloned.ExactModelMappings = make(map[string]string, len(value.ExactModelMappings)) for requestedModel, mappedModel := range value.ExactModelMappings { cloned.ExactModelMappings[requestedModel] = mappedModel } } return cloned } func cloneGroupForDuplicate(source *Group, operationID string) *Group { return &Group{ Name: duplicateGroupName(source.Name, 1), Description: source.Description, Platform: source.Platform, RateMultiplier: source.RateMultiplier, PeakRateEnabled: source.PeakRateEnabled, PeakStart: source.PeakStart, PeakEnd: source.PeakEnd, PeakRateMultiplier: source.PeakRateMultiplier, ProfitControlEnabled: source.ProfitControlEnabled, ProfitMinMargin: source.ProfitMinMargin, ProfitSafetyBuffer: source.ProfitSafetyBuffer, IsExclusive: source.IsExclusive, Status: duplicateGroupInactiveStatus, DuplicateOperationID: operationID, SubscriptionType: source.SubscriptionType, DailyLimitUSD: cloneGroupValuePointer(source.DailyLimitUSD), WeeklyLimitUSD: cloneGroupValuePointer(source.WeeklyLimitUSD), MonthlyLimitUSD: cloneGroupValuePointer(source.MonthlyLimitUSD), DefaultValidityDays: source.DefaultValidityDays, AllowImageGeneration: source.AllowImageGeneration, AllowBatchImageGeneration: source.AllowBatchImageGeneration, ImageRateIndependent: source.ImageRateIndependent, ImageRateMultiplier: source.ImageRateMultiplier, ImagePrice1K: cloneGroupValuePointer(source.ImagePrice1K), ImagePrice2K: cloneGroupValuePointer(source.ImagePrice2K), ImagePrice4K: cloneGroupValuePointer(source.ImagePrice4K), BatchImageDiscountMultiplier: source.BatchImageDiscountMultiplier, BatchImageHoldMultiplier: source.BatchImageHoldMultiplier, VideoRateIndependent: source.VideoRateIndependent, VideoRateMultiplier: source.VideoRateMultiplier, VideoPrice480P: cloneGroupValuePointer(source.VideoPrice480P), VideoPrice720P: cloneGroupValuePointer(source.VideoPrice720P), VideoPrice1080P: cloneGroupValuePointer(source.VideoPrice1080P), VideoModelPrices: cloneGroupVideoModelPrices(source.VideoModelPrices), WebSearchPricePerCall: cloneGroupValuePointer(source.WebSearchPricePerCall), SearchPricePer1k: cloneGroupValuePointer(source.SearchPricePer1k), AudioRealtimePricePerMin: cloneGroupValuePointer(source.AudioRealtimePricePerMin), AudioTTSPricePerMillionChars: cloneGroupValuePointer(source.AudioTTSPricePerMillionChars), AudioSTTPricePerHour: cloneGroupValuePointer(source.AudioSTTPricePerHour), ClaudeCodeOnly: source.ClaudeCodeOnly, FallbackGroupID: cloneGroupValuePointer(source.FallbackGroupID), FallbackGroupIDOnInvalidRequest: cloneGroupValuePointer(source.FallbackGroupIDOnInvalidRequest), ModelRouting: cloneGroupModelRouting(source.ModelRouting), ModelRoutingEnabled: source.ModelRoutingEnabled, MCPXMLInject: source.MCPXMLInject, SupportedModelScopes: append([]string(nil), source.SupportedModelScopes...), SortOrder: source.SortOrder, AllowMessagesDispatch: source.AllowMessagesDispatch, AllowLive: source.AllowLive, RequireOAuthOnly: source.RequireOAuthOnly, RequirePrivacySet: source.RequirePrivacySet, DefaultMappedModel: source.DefaultMappedModel, MessagesDispatchModelConfig: cloneGroupMessagesDispatchModelConfig(source.MessagesDispatchModelConfig), ModelsListConfig: GroupModelsListConfig{ Enabled: source.ModelsListConfig.Enabled, Models: append([]string(nil), source.ModelsListConfig.Models...), }, RPMLimit: source.RPMLimit, MaxReasoningEffort: source.MaxReasoningEffort, ReasoningEffortMappings: append([]ReasoningEffortMapping(nil), source.ReasoningEffortMappings...), } } // RecoverDuplicateGroup performs a read-only lookup for a copy that was already // committed for the same actor, source group, and idempotency key. func (s *adminServiceImpl) RecoverDuplicateGroup(ctx context.Context, id int64, actorScope, operationKey string) (*Group, error) { operationID := duplicateGroupOperationID(id, actorScope, operationKey) if operationID == "" { return nil, nil } if s.groupDuplicateRepo == nil { return nil, errors.New("group duplicate repository is not configured") } group, err := s.groupDuplicateRepo.FindByDuplicateOperationID(ctx, operationID) if err != nil { return nil, fmt.Errorf("find duplicate group operation: %w", err) } if group == nil { return nil, nil } hydrated, err := s.groupRepo.GetByID(ctx, group.ID) if err != nil { return nil, fmt.Errorf("load recovered duplicate group: %w", err) } return hydrated, nil } // DuplicateGroup creates an inactive copy of a group's configuration and exact // account priorities. The repository commits the group, bindings, and outbox // event atomically so a failed binding never leaves an orphan group. func (s *adminServiceImpl) DuplicateGroup(ctx context.Context, id int64, actorScope, operationKey string) (*Group, error) { existing, err := s.RecoverDuplicateGroup(ctx, id, actorScope, operationKey) if err != nil { return nil, err } if existing != nil { return existing, nil } source, err := s.groupRepo.GetByID(ctx, id) if err != nil { return nil, err } if s.groupDuplicateRepo == nil { return nil, errors.New("group duplicate repository is not configured") } duplicate := cloneGroupForDuplicate(source, duplicateGroupOperationID(id, actorScope, operationKey)) sanitizeGroupReasoningEffortPolicy(duplicate) for copyNumber := 1; ; copyNumber++ { duplicate.Name = duplicateGroupName(source.Name, copyNumber) duplicate.ID = 0 duplicate.CreatedAt = time.Time{} duplicate.UpdatedAt = time.Time{} if err := s.groupDuplicateRepo.CreateFromSource(ctx, duplicate, source.ID); err == nil { hydrated, loadErr := s.groupRepo.GetByID(ctx, duplicate.ID) if loadErr != nil { return nil, fmt.Errorf("load duplicate group: %w", loadErr) } return hydrated, nil } else if !errors.Is(err, ErrGroupExists) { return nil, fmt.Errorf("create duplicate group: %w", err) } // A unique conflict can be either the generated name or the operation ID. // Recover first; if no operation row exists, advance to the next name. recovered, recoverErr := s.RecoverDuplicateGroup(ctx, id, actorScope, operationKey) if recoverErr != nil { return nil, recoverErr } if recovered != nil { return recovered, nil } } }