grok_observed_models.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "io"
  6. "log/slog"
  7. "net/http"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  12. "github.com/tidwall/gjson"
  13. )
  14. const (
  15. grokObservedModelsExtraKey = "grok_observed_models"
  16. grokObservedModelsTTL = 6 * time.Hour
  17. grokObservedModelsTimeout = 15 * time.Second
  18. )
  19. type grokObservedModelsSnapshot struct {
  20. Models []string `json:"models"`
  21. FetchedAt string `json:"fetched_at"`
  22. Source string `json:"source,omitempty"`
  23. }
  24. var grokObservedModelsFlight sync.Map // accountID -> *singleflight-ish in-flight
  25. // scheduleGrokObservedModelsSync best-effort fetches upstream /v1/models for a
  26. // Grok OAuth account and stores IDs in Extra. Never blocks request path long;
  27. // callers should fire-and-forget after successful auth/probe.
  28. func (s *GrokQuotaService) scheduleGrokObservedModelsSync(account *Account) {
  29. if s == nil || account == nil || !account.IsGrokOAuth() || s.accountRepo == nil {
  30. return
  31. }
  32. id := account.ID
  33. if _, loaded := grokObservedModelsFlight.LoadOrStore(id, struct{}{}); loaded {
  34. return
  35. }
  36. // Copy credentials for background use.
  37. acc := *account
  38. go func() {
  39. defer grokObservedModelsFlight.Delete(id)
  40. ctx, cancel := context.WithTimeout(context.Background(), grokObservedModelsTimeout)
  41. defer cancel()
  42. if err := s.syncGrokObservedModels(ctx, &acc); err != nil {
  43. slog.Debug("grok_observed_models_sync_failed", "account_id", id, "error", err)
  44. }
  45. }()
  46. }
  47. func (s *GrokQuotaService) syncGrokObservedModels(ctx context.Context, account *Account) error {
  48. if s == nil || account == nil {
  49. return nil
  50. }
  51. // Skip if snapshot is still fresh.
  52. if snap := parseGrokObservedModels(account.Extra); snap != nil {
  53. if t, err := time.Parse(time.RFC3339, snap.FetchedAt); err == nil && time.Since(t) < grokObservedModelsTTL {
  54. return nil
  55. }
  56. }
  57. token := strings.TrimSpace(account.GetGrokAccessToken())
  58. if token == "" && s.tokenProvider != nil {
  59. // Best-effort warm; avoid forcing refresh storms.
  60. if at, err := s.tokenProvider.GetAccessToken(ctx, account); err == nil {
  61. token = strings.TrimSpace(at)
  62. }
  63. }
  64. if token == "" {
  65. return nil
  66. }
  67. baseURL := strings.TrimSpace(account.GetGrokBaseURL())
  68. if s.settingService != nil {
  69. baseURL = strings.TrimSpace(s.settingService.ResolveGrokBaseURL(ctx, account))
  70. }
  71. if baseURL == "" {
  72. baseURL = xai.DefaultCLIBaseURL
  73. }
  74. validator, err := grokBaseURLValidator(account, s.cfg)
  75. if err != nil {
  76. return err
  77. }
  78. validatedBaseURL, err := validator(baseURL)
  79. if err != nil {
  80. return err
  81. }
  82. req, err := http.NewRequestWithContext(ctx, http.MethodGet, buildOpenAIModelsURL(validatedBaseURL), nil)
  83. if err != nil {
  84. return err
  85. }
  86. req.Header.Set("Authorization", "Bearer "+token)
  87. req.Header.Set("Accept", "application/json")
  88. req.Header.Set("User-Agent", defaultGrokUpstreamUserAgent())
  89. if account.IsGrokOAuth() {
  90. applyGrokCLIHeaders(req.Header)
  91. if isGrokCLIProxyTarget(req.URL.String()) {
  92. if userID := strings.TrimSpace(account.GetCredential("sub")); userID != "" {
  93. req.Header.Set("X-UserID", userID)
  94. }
  95. if email := strings.TrimSpace(account.GetCredential("email")); email != "" {
  96. req.Header.Set("X-Email", email)
  97. }
  98. }
  99. }
  100. account.ApplyHeaderOverrides(req.Header)
  101. proxyURL := ""
  102. if s.proxyRepo != nil && account.ProxyID != nil {
  103. if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil {
  104. proxyURL = p.URL()
  105. }
  106. }
  107. if s.httpUpstream == nil {
  108. return nil
  109. }
  110. resp, err := s.httpUpstream.Do(req, proxyURL, account.ID, account.Concurrency)
  111. if err != nil {
  112. return err
  113. }
  114. defer func() { _ = resp.Body.Close() }()
  115. body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
  116. if err != nil {
  117. return err
  118. }
  119. if resp.StatusCode >= 400 {
  120. return nil
  121. }
  122. ids := extractGrokModelIDsFromModelsBody(body)
  123. if len(ids) == 0 {
  124. return nil
  125. }
  126. snap := grokObservedModelsSnapshot{
  127. Models: ids,
  128. FetchedAt: time.Now().UTC().Format(time.RFC3339),
  129. Source: "upstream_v1_models",
  130. }
  131. raw, err := json.Marshal(snap)
  132. if err != nil {
  133. return err
  134. }
  135. var asMap map[string]any
  136. if err := json.Unmarshal(raw, &asMap); err != nil {
  137. return err
  138. }
  139. return s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{
  140. grokObservedModelsExtraKey: asMap,
  141. })
  142. }
  143. func extractGrokModelIDsFromModelsBody(body []byte) []string {
  144. data := gjson.GetBytes(body, "data")
  145. if !data.IsArray() {
  146. // Some gateways return a bare array.
  147. data = gjson.ParseBytes(body)
  148. }
  149. seen := make(map[string]struct{})
  150. var out []string
  151. data.ForEach(func(_, v gjson.Result) bool {
  152. id := strings.TrimSpace(v.Get("id").String())
  153. if id == "" {
  154. id = strings.TrimSpace(v.String())
  155. }
  156. if id == "" {
  157. return true
  158. }
  159. if _, ok := seen[id]; ok {
  160. return true
  161. }
  162. seen[id] = struct{}{}
  163. out = append(out, id)
  164. return true
  165. })
  166. return out
  167. }
  168. func parseGrokObservedModels(extra map[string]any) *grokObservedModelsSnapshot {
  169. if extra == nil {
  170. return nil
  171. }
  172. raw, ok := extra[grokObservedModelsExtraKey]
  173. if !ok || raw == nil {
  174. return nil
  175. }
  176. b, err := json.Marshal(raw)
  177. if err != nil {
  178. return nil
  179. }
  180. var snap grokObservedModelsSnapshot
  181. if err := json.Unmarshal(b, &snap); err != nil {
  182. return nil
  183. }
  184. if len(snap.Models) == 0 {
  185. return nil
  186. }
  187. return &snap
  188. }