gateway_profit_control.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package service
  2. import (
  3. "context"
  4. "log/slog"
  5. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  6. )
  7. // withGatewayProfitControlGate installs the gate only for explicitly marked
  8. // token requests. This keeps media, metadata, and models-list paths outside
  9. // the profit-control surface by construction.
  10. func (s *GatewayService) withGatewayProfitControlGate(ctx context.Context, groupID *int64) context.Context {
  11. if _, ok := gatewayTokenRequestPricingAtFromContext(ctx); !ok || groupID == nil || *groupID <= 0 {
  12. return ctx
  13. }
  14. if existing, ok := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate); ok && existing != nil && existing.groupID == *groupID {
  15. return ctx
  16. }
  17. group, err := s.resolveProfitControlGroup(ctx, *groupID)
  18. if err != nil {
  19. slog.Warn("profit_control_group_load_failed", "group_id", *groupID, "error", err)
  20. return s.clearForeignProfitControlGate(ctx, groupID)
  21. }
  22. if group == nil || !group.ProfitControlEnabled || !profitControlPlatformSupported(group.Platform) {
  23. return s.clearForeignProfitControlGate(ctx, groupID)
  24. }
  25. pricingAt, _ := gatewayTokenRequestPricingAtFromContext(ctx)
  26. billingGroup := gatewayTokenRequestBillingGroupFromContext(ctx)
  27. if billingGroup == nil {
  28. if ctxGroup, ok := ctx.Value(ctxkey.Group).(*Group); ok && IsGroupContextValid(ctxGroup) {
  29. billingGroup = ctxGroup
  30. } else {
  31. billingGroup = group
  32. }
  33. }
  34. downstream := billingGroup.RateMultiplier
  35. if userID, _ := ctx.Value(ctxkey.UserID).(int64); userID > 0 {
  36. downstream = s.ResolveUserGroupRateMultiplier(ctx, userID, billingGroup.ID, billingGroup.RateMultiplier)
  37. }
  38. downstream *= billingGroup.PeakMultiplierAt(pricingAt)
  39. threshold := clampProfitControlThreshold(downstream * (1 - group.ProfitMinMargin - group.ProfitSafetyBuffer))
  40. gate := &openAIProfitControlGate{
  41. groupID: group.ID,
  42. platform: group.Platform,
  43. threshold: threshold,
  44. pricingAt: pricingAt,
  45. }
  46. openAIProfitControlObserverInstance.recordInstall(gate.groupID, gate.platform, gate.threshold)
  47. return context.WithValue(ctx, openAIProfitControlGateCtxKey{}, gate)
  48. }
  49. func (s *GatewayService) clearForeignProfitControlGate(ctx context.Context, groupID *int64) context.Context {
  50. existing, ok := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate)
  51. if !ok || existing == nil || groupID == nil || existing.groupID == *groupID {
  52. return ctx
  53. }
  54. return context.WithValue(ctx, openAIProfitControlGateCtxKey{}, (*openAIProfitControlGate)(nil))
  55. }
  56. func (s *GatewayService) resolveProfitControlGroup(ctx context.Context, groupID int64) (*Group, error) {
  57. if group, ok := ctx.Value(ctxkey.Group).(*Group); ok && IsGroupContextValid(group) && group.ID == groupID {
  58. return group, nil
  59. }
  60. if s.schedulerSnapshot != nil {
  61. // Lite 读取:门只用平台/倍率/利润/高峰字段,不需要账号计数聚合。
  62. return s.schedulerSnapshot.GetGroupByIDLite(ctx, groupID)
  63. }
  64. return s.resolveGroupByID(ctx, groupID)
  65. }
  66. // GatewayProfitControlVetoLatest performs the terminal post-slot check against
  67. // the latest scheduler snapshot. Snapshot read failures are deliberately
  68. // fail-open to preserve availability, but are observable.
  69. func (s *GatewayService) GatewayProfitControlVetoLatest(ctx context.Context, selected *Account) (*Account, bool, string) {
  70. return profitControlVetoLatest(ctx, selected, s.schedulerSnapshot)
  71. }
  72. func profitControlVetoLatest(ctx context.Context, selected *Account, snapshot *SchedulerSnapshotService) (*Account, bool, string) {
  73. gate, _ := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate)
  74. if gate == nil || selected == nil {
  75. return selected, false, ""
  76. }
  77. latest := selected
  78. if snapshot != nil {
  79. refreshed, err := snapshot.GetAccount(ctx, selected.ID)
  80. if err != nil || refreshed == nil {
  81. slog.Warn("profit_control_account_refresh_failed", "group_id", gate.groupID, "platform", gate.platform, "account_id", selected.ID, "error", err)
  82. openAIProfitControlObserverInstance.recordRefreshFailure(gate.groupID, gate.platform, gate.threshold)
  83. } else if !refreshed.UpdatedAt.Before(selected.UpdatedAt) {
  84. // 选号路径可能已做过 DB recheck,selected 比缓存快照更新鲜;只有
  85. // 快照不落后时才替换,避免终检把新鲜账号换回较旧的缓存对象。
  86. latest = refreshed
  87. }
  88. }
  89. vetoed, reason := openAIProfitControlVetoReason(ctx, latest)
  90. return latest, vetoed, reason
  91. }
  92. func (s *GatewayService) isGatewayAccountProfitEligible(ctx context.Context, account *Account) bool {
  93. vetoed, _ := openAIProfitControlVetoReason(ctx, account)
  94. return !vetoed
  95. }
  96. func gatewayProfitControlGateActive(ctx context.Context) bool {
  97. gate, _ := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate)
  98. return gate != nil
  99. }