model_rate_limit.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  7. )
  8. const (
  9. modelRateLimitsKey = "model_rate_limits"
  10. antigravityGeminiModelRateLimitKey = "antigravity:gemini"
  11. openAIImageGenerationRateLimitKey = "openai:image_generation"
  12. openAICodexSparkRateLimitReason = "openai_codex_spark_rate_limit"
  13. // anthropicFableRateLimitKey 是 Anthropic 7d_oi(Fable 专属 7d 窗口)限流的
  14. // 家族级 scope:命中后所有 Fable 变体(含 [1m] 等后缀)都不再调度到该账号。
  15. anthropicFableRateLimitKey = "claude-fable-5"
  16. )
  17. // isRateLimitActiveForKey 检查指定 key 的限流是否生效
  18. func (a *Account) isRateLimitActiveForKey(key string) bool {
  19. resetAt := a.modelRateLimitResetAt(key)
  20. return resetAt != nil && time.Now().Before(*resetAt)
  21. }
  22. // getRateLimitRemainingForKey 获取指定 key 的限流剩余时间,0 表示未限流或已过期
  23. func (a *Account) getRateLimitRemainingForKey(key string) time.Duration {
  24. resetAt := a.modelRateLimitResetAt(key)
  25. if resetAt == nil {
  26. return 0
  27. }
  28. remaining := time.Until(*resetAt)
  29. if remaining > 0 {
  30. return remaining
  31. }
  32. return 0
  33. }
  34. func (a *Account) isModelRateLimitedWithContext(ctx context.Context, requestedModel string) bool {
  35. for _, key := range a.modelRateLimitKeysForRequest(ctx, requestedModel) {
  36. if a.isRateLimitActiveForKey(key) {
  37. return true
  38. }
  39. }
  40. return false
  41. }
  42. // GetModelRateLimitRemainingTime 获取模型限流剩余时间
  43. // 返回 0 表示未限流或已过期
  44. func (a *Account) GetModelRateLimitRemainingTime(requestedModel string) time.Duration {
  45. return a.GetModelRateLimitRemainingTimeWithContext(context.Background(), requestedModel)
  46. }
  47. func (a *Account) GetModelRateLimitRemainingTimeWithContext(ctx context.Context, requestedModel string) time.Duration {
  48. remaining := time.Duration(0)
  49. for _, key := range a.modelRateLimitKeysForRequest(ctx, requestedModel) {
  50. if keyRemaining := a.getRateLimitRemainingForKey(key); keyRemaining > remaining {
  51. remaining = keyRemaining
  52. }
  53. }
  54. return remaining
  55. }
  56. func (a *Account) modelRateLimitKeysForRequest(ctx context.Context, requestedModel string) []string {
  57. if a == nil {
  58. return nil
  59. }
  60. modelKey := a.GetMappedModel(requestedModel)
  61. if a.Platform == PlatformAntigravity {
  62. modelKey = resolveFinalAntigravityModelKey(ctx, a, requestedModel)
  63. }
  64. modelKey = strings.TrimSpace(modelKey)
  65. if modelKey == "" {
  66. return nil
  67. }
  68. keys := []string{modelKey}
  69. switch a.Platform {
  70. case PlatformAntigravity:
  71. if isAntigravityGeminiModel(modelKey) && modelKey != antigravityGeminiModelRateLimitKey {
  72. keys = append(keys, antigravityGeminiModelRateLimitKey)
  73. }
  74. case PlatformOpenAI:
  75. if openAIImageGenerationRateLimitApplies(ctx, requestedModel, modelKey) && modelKey != openAIImageGenerationRateLimitKey {
  76. keys = append(keys, openAIImageGenerationRateLimitKey)
  77. }
  78. case PlatformAnthropic:
  79. if isAnthropicFableModel(modelKey) && modelKey != anthropicFableRateLimitKey {
  80. keys = append(keys, anthropicFableRateLimitKey)
  81. }
  82. }
  83. return keys
  84. }
  85. // isAnthropicFableModel 判断是否为 Fable 模型家族(claude-fable-5、claude-fable-5[1m] 等变体)
  86. func isAnthropicFableModel(model string) bool {
  87. return strings.Contains(strings.ToLower(model), "fable")
  88. }
  89. func openAIImageGenerationRateLimitApplies(ctx context.Context, requestedModel, modelKey string) bool {
  90. if isOpenAIImageGenerationModel(requestedModel) || isOpenAIImageGenerationModel(modelKey) {
  91. return true
  92. }
  93. return OpenAIImageGenerationIntentFromContext(ctx)
  94. }
  95. func WithOpenAIImageGenerationIntent(ctx context.Context) context.Context {
  96. if ctx == nil {
  97. ctx = context.Background()
  98. }
  99. return context.WithValue(ctx, ctxkey.OpenAIImageGenerationIntent, true)
  100. }
  101. func OpenAIImageGenerationIntentFromContext(ctx context.Context) bool {
  102. if ctx == nil {
  103. return false
  104. }
  105. enabled, ok := ctx.Value(ctxkey.OpenAIImageGenerationIntent).(bool)
  106. return ok && enabled
  107. }
  108. // WithOpenAIImagesEndpoint 标记请求从 /v1/images/* 专用生图端点入站。
  109. func WithOpenAIImagesEndpoint(ctx context.Context) context.Context {
  110. if ctx == nil {
  111. ctx = context.Background()
  112. }
  113. return context.WithValue(ctx, ctxkey.OpenAIImagesEndpoint, true)
  114. }
  115. // OpenAIImagesEndpointFromContext 报告请求是否来自 /v1/images/*。
  116. func OpenAIImagesEndpointFromContext(ctx context.Context) bool {
  117. if ctx == nil {
  118. return false
  119. }
  120. enabled, ok := ctx.Value(ctxkey.OpenAIImagesEndpoint).(bool)
  121. return ok && enabled
  122. }
  123. func resolveFinalAntigravityModelKey(ctx context.Context, account *Account, requestedModel string) string {
  124. modelKey := mapAntigravityModel(account, requestedModel)
  125. if modelKey == "" {
  126. return ""
  127. }
  128. // thinking 会影响 Antigravity 最终模型名(例如 claude-sonnet-4-5 -> claude-sonnet-4-5-thinking)
  129. if enabled, ok := ThinkingEnabledFromContext(ctx); ok {
  130. modelKey = applyThinkingModelSuffix(modelKey, enabled)
  131. }
  132. return modelKey
  133. }
  134. func isAntigravityGeminiModel(model string) bool {
  135. return strings.HasPrefix(normalizeAntigravityModelName(model), "gemini-")
  136. }
  137. func antigravityModelRateLimitKeys(model string) []string {
  138. model = strings.TrimSpace(model)
  139. if model == "" {
  140. return nil
  141. }
  142. keys := []string{model}
  143. if isAntigravityGeminiModel(model) && model != antigravityGeminiModelRateLimitKey {
  144. keys = append(keys, antigravityGeminiModelRateLimitKey)
  145. }
  146. return keys
  147. }
  148. func (a *Account) modelRateLimitResetAt(scope string) *time.Time {
  149. if a == nil || a.Extra == nil || scope == "" {
  150. return nil
  151. }
  152. rawLimits, ok := a.Extra[modelRateLimitsKey].(map[string]any)
  153. if !ok {
  154. return nil
  155. }
  156. rawLimit, ok := rawLimits[scope].(map[string]any)
  157. if !ok {
  158. return nil
  159. }
  160. resetAtRaw, ok := rawLimit["rate_limit_reset_at"].(string)
  161. if !ok || strings.TrimSpace(resetAtRaw) == "" {
  162. return nil
  163. }
  164. resetAt, err := time.Parse(time.RFC3339, resetAtRaw)
  165. if err != nil {
  166. return nil
  167. }
  168. return &resetAt
  169. }
  170. func setAccountModelRateLimitSnapshot(account *Account, scope string, resetAt time.Time, reason string, now time.Time) {
  171. if account == nil || strings.TrimSpace(scope) == "" {
  172. return
  173. }
  174. if account.Extra == nil {
  175. account.Extra = make(map[string]any)
  176. }
  177. limits, ok := account.Extra[modelRateLimitsKey].(map[string]any)
  178. if !ok {
  179. limits = make(map[string]any)
  180. account.Extra[modelRateLimitsKey] = limits
  181. }
  182. payload := map[string]any{
  183. "rate_limited_at": now.UTC().Format(time.RFC3339),
  184. "rate_limit_reset_at": resetAt.UTC().Format(time.RFC3339),
  185. }
  186. if reason = strings.TrimSpace(reason); reason != "" {
  187. payload["reason"] = reason
  188. }
  189. limits[scope] = payload
  190. }