openai_quota_auto_reset_config.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package service
  2. import (
  3. "encoding/json"
  4. "math"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. )
  10. const (
  11. OpenAIAutoResetCreditEnabledExtraKey = "auto_reset_credit_enabled"
  12. OpenAIAutoResetCredit5hThresholdExtraKey = "auto_reset_credit_5h_threshold"
  13. OpenAIAutoResetCredit7dThresholdExtraKey = "auto_reset_credit_7d_threshold"
  14. OpenAIAutoResetCreditStateExtraKey = "codex_auto_reset_credit_state"
  15. openAIAutoResetCreditDefaultThreshold = 1.0
  16. openAIAutoResetCreditMinimumThreshold = 0.001
  17. )
  18. // OpenAIAutoResetCreditConfig 是账号级自动用卡配置。阈值采用 0-1 比例,
  19. // 避免后端调度与前端百分比展示混用同一数值语义。
  20. type OpenAIAutoResetCreditConfig struct {
  21. Enabled bool
  22. Threshold5h float64
  23. Threshold7d float64
  24. }
  25. // ResolveOpenAIAutoResetCreditConfig 只接受 OpenAI OAuth 母账号;历史账号未配置时
  26. // 始终保持关闭,防止升级后产生意外消费。
  27. func ResolveOpenAIAutoResetCreditConfig(account *Account) OpenAIAutoResetCreditConfig {
  28. config := OpenAIAutoResetCreditConfig{
  29. Threshold5h: openAIAutoResetCreditDefaultThreshold,
  30. Threshold7d: openAIAutoResetCreditDefaultThreshold,
  31. }
  32. if !isOpenAIAutoResetCreditAccount(account) || account.Extra == nil {
  33. return config
  34. }
  35. config.Enabled = resolveAccountExtraBool(account.Extra, OpenAIAutoResetCreditEnabledExtraKey)
  36. if value, ok := resolveAccountExtraNumber(account.Extra, OpenAIAutoResetCredit5hThresholdExtraKey); ok && isValidOpenAIAutoResetThreshold(value) {
  37. config.Threshold5h = value
  38. }
  39. if value, ok := resolveAccountExtraNumber(account.Extra, OpenAIAutoResetCredit7dThresholdExtraKey); ok && isValidOpenAIAutoResetThreshold(value) {
  40. config.Threshold7d = value
  41. }
  42. return config
  43. }
  44. func isOpenAIAutoResetCreditAccount(account *Account) bool {
  45. return account != nil && account.Platform == PlatformOpenAI && account.Type == AccountTypeOAuth && !account.IsShadow()
  46. }
  47. // normalizeOpenAIAutoResetCreditExtra 校验管理请求中的配置并剥离服务运行态。
  48. // enabled=true 时补齐两个 100% 默认阈值;关闭时保留已设置阈值,方便再次开启。
  49. func normalizeOpenAIAutoResetCreditExtra(platform, accountType string, isShadow bool, extra map[string]any) (map[string]any, error) {
  50. if extra == nil {
  51. return nil, nil
  52. }
  53. normalized := cloneOpenAIAutoResetExtra(extra)
  54. delete(normalized, OpenAIAutoResetCreditStateExtraKey)
  55. _, hasEnabled := normalized[OpenAIAutoResetCreditEnabledExtraKey]
  56. _, has5h := normalized[OpenAIAutoResetCredit5hThresholdExtraKey]
  57. _, has7d := normalized[OpenAIAutoResetCredit7dThresholdExtraKey]
  58. if !hasEnabled && !has5h && !has7d {
  59. return normalized, nil
  60. }
  61. if platform != PlatformOpenAI || accountType != AccountTypeOAuth || isShadow {
  62. return nil, infraerrors.New(http.StatusBadRequest, "OPENAI_AUTO_RESET_CREDIT_ACCOUNT_INVALID", "automatic reset credits are only supported for OpenAI OAuth parent accounts")
  63. }
  64. enabled := false
  65. if hasEnabled {
  66. value, ok := normalized[OpenAIAutoResetCreditEnabledExtraKey].(bool)
  67. if !ok {
  68. return nil, infraerrors.New(http.StatusBadRequest, "OPENAI_AUTO_RESET_CREDIT_ENABLED_INVALID", "auto_reset_credit_enabled must be a boolean")
  69. }
  70. enabled = value
  71. }
  72. for key, present := range map[string]bool{
  73. OpenAIAutoResetCredit5hThresholdExtraKey: has5h,
  74. OpenAIAutoResetCredit7dThresholdExtraKey: has7d,
  75. } {
  76. if !present {
  77. if enabled {
  78. normalized[key] = openAIAutoResetCreditDefaultThreshold
  79. }
  80. continue
  81. }
  82. value, ok := parseOpenAIAutoResetThreshold(normalized[key])
  83. if !ok || !isValidOpenAIAutoResetThreshold(value) {
  84. return nil, infraerrors.Newf(http.StatusBadRequest, "OPENAI_AUTO_RESET_CREDIT_THRESHOLD_INVALID", "%s must be between 0.001 and 1.0", key)
  85. }
  86. normalized[key] = value
  87. }
  88. return normalized, nil
  89. }
  90. func stripOpenAIAutoResetCreditManagedExtra(extra map[string]any, stripConfig bool) map[string]any {
  91. if extra == nil {
  92. return nil
  93. }
  94. delete(extra, OpenAIAutoResetCreditStateExtraKey)
  95. if stripConfig {
  96. delete(extra, OpenAIAutoResetCreditEnabledExtraKey)
  97. delete(extra, OpenAIAutoResetCredit5hThresholdExtraKey)
  98. delete(extra, OpenAIAutoResetCredit7dThresholdExtraKey)
  99. }
  100. return extra
  101. }
  102. func parseOpenAIAutoResetThreshold(value any) (float64, bool) {
  103. switch typed := value.(type) {
  104. case float64:
  105. return typed, true
  106. case float32:
  107. return float64(typed), true
  108. case int:
  109. return float64(typed), true
  110. case int64:
  111. return float64(typed), true
  112. case json.Number:
  113. parsed, err := typed.Float64()
  114. return parsed, err == nil
  115. case string:
  116. parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
  117. return parsed, err == nil
  118. default:
  119. return 0, false
  120. }
  121. }
  122. func isValidOpenAIAutoResetThreshold(value float64) bool {
  123. return !math.IsNaN(value) && !math.IsInf(value, 0) && value >= openAIAutoResetCreditMinimumThreshold && value <= 1
  124. }
  125. func cloneOpenAIAutoResetExtra(source map[string]any) map[string]any {
  126. if source == nil {
  127. return nil
  128. }
  129. cloned := make(map[string]any, len(source))
  130. for key, value := range source {
  131. cloned[key] = value
  132. }
  133. return cloned
  134. }