grok_model_quota_block.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package service
  2. import (
  3. "strconv"
  4. "strings"
  5. "sync"
  6. "time"
  7. )
  8. // Process-local per-account model soft-blocks for Grok free-usage that names a
  9. // model (e.g. "used all free usage for model grok-4.5"). Sibling models on the
  10. // same account stay schedulable. Multi-instance: each process learns from its
  11. // own upstream errors.
  12. type grokModelQuotaBlock struct {
  13. Until time.Time
  14. }
  15. type grokModelQuotaBlockStore struct {
  16. mu sync.Mutex
  17. items map[string]grokModelQuotaBlock // key: accountID|model
  18. }
  19. var globalGrokModelQuotaBlocks = &grokModelQuotaBlockStore{
  20. items: make(map[string]grokModelQuotaBlock),
  21. }
  22. const (
  23. grokModelQuotaBlockDefaultTTL = 2 * time.Hour
  24. grokModelQuotaBlockMaxTTL = 6 * time.Hour
  25. grokModelQuotaBlockMinTTL = 20 * time.Minute
  26. )
  27. func grokModelQuotaBlockKey(accountID int64, model string) string {
  28. return strings.TrimSpace(strings.ToLower(model)) + "|" + strconv.FormatInt(accountID, 10)
  29. }
  30. // markGrokModelQuotaBlock soft-blocks accountID for model until the given time.
  31. func markGrokModelQuotaBlock(accountID int64, model string, until time.Time) {
  32. model = strings.TrimSpace(model)
  33. if accountID <= 0 || model == "" || until.IsZero() {
  34. return
  35. }
  36. now := time.Now()
  37. if !until.After(now.Add(grokModelQuotaBlockMinTTL)) {
  38. until = now.Add(grokModelQuotaBlockDefaultTTL)
  39. }
  40. if max := now.Add(grokModelQuotaBlockMaxTTL); until.After(max) {
  41. until = max
  42. }
  43. storeGrokModelQuotaBlock(accountID, model, until, now)
  44. }
  45. const (
  46. grokModelTransientBlockMinTTL = 500 * time.Millisecond
  47. grokModelTransientBlockMaxTTL = 5 * time.Minute
  48. )
  49. // markGrokModelTransientBlock soft-blocks a single model for a short capacity
  50. // burst without the free-usage 20m floor (and without unscheduling the account).
  51. func markGrokModelTransientBlock(accountID int64, model string, until time.Time) {
  52. model = strings.TrimSpace(model)
  53. if accountID <= 0 || model == "" || until.IsZero() {
  54. return
  55. }
  56. now := time.Now()
  57. if !until.After(now.Add(grokModelTransientBlockMinTTL)) {
  58. until = now.Add(grokModelTransientBlockMinTTL)
  59. }
  60. if max := now.Add(grokModelTransientBlockMaxTTL); until.After(max) {
  61. until = max
  62. }
  63. storeGrokModelQuotaBlock(accountID, model, until, now)
  64. }
  65. func storeGrokModelQuotaBlock(accountID int64, model string, until, now time.Time) {
  66. key := grokModelQuotaBlockKey(accountID, model)
  67. globalGrokModelQuotaBlocks.mu.Lock()
  68. defer globalGrokModelQuotaBlocks.mu.Unlock()
  69. if cur, ok := globalGrokModelQuotaBlocks.items[key]; ok && cur.Until.After(until) {
  70. return
  71. }
  72. globalGrokModelQuotaBlocks.items[key] = grokModelQuotaBlock{Until: until}
  73. for k, v := range globalGrokModelQuotaBlocks.items {
  74. if !v.Until.After(now) {
  75. delete(globalGrokModelQuotaBlocks.items, k)
  76. }
  77. }
  78. }
  79. // isGrokModelQuotaBlocked reports whether this account cannot serve model now.
  80. func isGrokModelQuotaBlocked(accountID int64, model string, now time.Time) bool {
  81. model = strings.TrimSpace(model)
  82. if accountID <= 0 || model == "" {
  83. return false
  84. }
  85. key := grokModelQuotaBlockKey(accountID, model)
  86. globalGrokModelQuotaBlocks.mu.Lock()
  87. defer globalGrokModelQuotaBlocks.mu.Unlock()
  88. cur, ok := globalGrokModelQuotaBlocks.items[key]
  89. if !ok {
  90. return false
  91. }
  92. if !cur.Until.After(now) {
  93. delete(globalGrokModelQuotaBlocks.items, key)
  94. return false
  95. }
  96. return true
  97. }
  98. func filterGrokModelQuotaBlockedAccounts(accounts []Account, model string, now time.Time) []Account {
  99. if len(accounts) == 0 || strings.TrimSpace(model) == "" {
  100. return accounts
  101. }
  102. out := make([]Account, 0, len(accounts))
  103. for i := range accounts {
  104. upstreamModel := canonicalOpenAIAccountSchedulingModel(&accounts[i], model)
  105. if isGrokModelQuotaBlocked(accounts[i].ID, upstreamModel, now) {
  106. continue
  107. }
  108. out = append(out, accounts[i])
  109. }
  110. return out
  111. }
  112. // isGrokModelSpecificFreeUsage is true when free-usage exhaustion is scoped to
  113. // a named model (account may still serve other models).
  114. func isGrokModelSpecificFreeUsage(low, model string) bool {
  115. model = strings.ToLower(strings.TrimSpace(model))
  116. if model == "" || low == "" {
  117. return false
  118. }
  119. if strings.Contains(low, "for model") || strings.Contains(low, "模型") {
  120. return true
  121. }
  122. // "used all the included free usage for model grok-4.5"
  123. if strings.Contains(low, "free usage") && strings.Contains(low, model) {
  124. return true
  125. }
  126. return false
  127. }