mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-27 16:07:02 +00:00
Persist reasoning ceilings and exact mappings for OpenAI groups, enforce them across HTTP and WebSocket forwarding, and invalidate cached auth snapshots.
209 lines
7.9 KiB
Go
209 lines
7.9 KiB
Go
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 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,
|
|
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),
|
|
WebSearchPricePerCall: cloneGroupValuePointer(source.WebSearchPricePerCall),
|
|
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,
|
|
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
|
|
}
|
|
}
|
|
}
|