gateway_model_availability.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "github.com/Wei-Shaw/sub2api/internal/config"
  6. )
  7. // ModelAvailabilityDiagnosis describes whether the requested model can be
  8. // served by any persistently eligible account in the group (active with its
  9. // schedulable setting enabled), ignoring transient state such as rate limits,
  10. // overload, temporary unschedulability, and runtime blocks. Handlers use this
  11. // on the "no available accounts" error path to distinguish 404
  12. // model_not_found from 503 service_unavailable.
  13. type ModelAvailabilityDiagnosis struct {
  14. // HasAccountsInPool is true if the group has at least one persistently
  15. // eligible account on the queried platform (or, for Anthropic/Gemini, on
  16. // the platform plus mixed-scheduled Antigravity accounts).
  17. HasAccountsInPool bool
  18. // HasModelSupport is true if at least one account's model mapping admits
  19. // the requested model.
  20. HasModelSupport bool
  21. }
  22. // ModelAvailabilityDiagnoser is implemented by gateway services that can
  23. // report whether the requested model is configured to be served by any
  24. // account. Both *GatewayService and *OpenAIGatewayService implement this so
  25. // handlers in either package can share a single classifier.
  26. type ModelAvailabilityDiagnoser interface {
  27. DiagnoseModelAvailabilityForPlatform(
  28. ctx context.Context,
  29. groupID *int64,
  30. requestedModel string,
  31. platform string,
  32. ) ModelAvailabilityDiagnosis
  33. }
  34. // DiagnoseModelAvailabilityForPlatform inspects accounts enabled for scheduling
  35. // by persistent configuration and returns whether the requested model is
  36. // configured to be served by any of them. The dedicated repository query
  37. // bypasses scheduler snapshots and deliberately ignores transient rate-limit,
  38. // overload, temporary-unschedulable, expiry, quota, and runtime-block state.
  39. //
  40. // Safe to call on the error path: returns {true,true} on any internal failure
  41. // or when the inputs preclude meaningful diagnosis (empty model, etc.), so
  42. // callers stay on the 503 fallback branch.
  43. func (s *GatewayService) DiagnoseModelAvailabilityForPlatform(
  44. ctx context.Context,
  45. groupID *int64,
  46. requestedModel string,
  47. platform string,
  48. ) ModelAvailabilityDiagnosis {
  49. if s == nil {
  50. return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
  51. }
  52. requestedModel = strings.TrimSpace(requestedModel)
  53. if requestedModel == "" {
  54. // No model specified — cannot decide model_not_found. Caller falls back to 503.
  55. return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
  56. }
  57. if strings.TrimSpace(platform) == "" {
  58. // Without a platform we cannot scope the lookup; bail out to the
  59. // 503 branch rather than make an unscoped scan.
  60. return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
  61. }
  62. if s.accountRepo == nil {
  63. return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
  64. }
  65. useMixed := platform == PlatformAnthropic || platform == PlatformGemini
  66. platforms := []string{platform}
  67. if useMixed {
  68. platforms = append(platforms, PlatformAntigravity)
  69. }
  70. queryGroupID := groupID
  71. includeGrouped := false
  72. if useMixed {
  73. // Preserve the generic scheduler's scope rules: an explicit group wins
  74. // for mixed scheduling, while group-less simple mode scans all accounts.
  75. if groupID == nil && s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
  76. includeGrouped = true
  77. }
  78. } else if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
  79. queryGroupID = nil
  80. includeGrouped = true
  81. }
  82. accounts, err := s.accountRepo.ListModelAvailabilityCandidates(ctx, queryGroupID, platforms, includeGrouped)
  83. if err != nil {
  84. // Conservative fallback: pretend everything is fine so the caller
  85. // returns 503 (we don't want to flip to 404 just because a lookup
  86. // hiccup'd).
  87. return ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}
  88. }
  89. diag := ModelAvailabilityDiagnosis{}
  90. for i := range accounts {
  91. if useMixed && accounts[i].Platform == PlatformAntigravity && !accounts[i].IsMixedSchedulingEnabled() {
  92. continue
  93. }
  94. diag.HasAccountsInPool = true
  95. if s.isModelSupportedByAccountWithContext(ctx, &accounts[i], requestedModel) {
  96. diag.HasModelSupport = true
  97. return diag
  98. }
  99. }
  100. return diag
  101. }