gemini_token_provider.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "log"
  6. "log/slog"
  7. "strconv"
  8. "strings"
  9. "time"
  10. )
  11. const (
  12. geminiTokenRefreshSkew = 3 * time.Minute
  13. geminiTokenCacheSkew = 5 * time.Minute
  14. )
  15. // GeminiTokenProvider manages access_token for Gemini OAuth and Vertex service account accounts.
  16. type GeminiTokenProvider struct {
  17. accountRepo AccountRepository
  18. tokenCache GeminiTokenCache
  19. geminiOAuthService *GeminiOAuthService
  20. refreshAPI *OAuthRefreshAPI
  21. executor OAuthRefreshExecutor
  22. refreshPolicy ProviderRefreshPolicy
  23. }
  24. func NewGeminiTokenProvider(
  25. accountRepo AccountRepository,
  26. tokenCache GeminiTokenCache,
  27. geminiOAuthService *GeminiOAuthService,
  28. ) *GeminiTokenProvider {
  29. return &GeminiTokenProvider{
  30. accountRepo: accountRepo,
  31. tokenCache: tokenCache,
  32. geminiOAuthService: geminiOAuthService,
  33. refreshPolicy: GeminiProviderRefreshPolicy(),
  34. }
  35. }
  36. // SetRefreshAPI injects unified OAuth refresh API and executor.
  37. func (p *GeminiTokenProvider) SetRefreshAPI(api *OAuthRefreshAPI, executor OAuthRefreshExecutor) {
  38. p.refreshAPI = api
  39. p.executor = executor
  40. }
  41. // SetRefreshPolicy injects caller-side refresh policy.
  42. func (p *GeminiTokenProvider) SetRefreshPolicy(policy ProviderRefreshPolicy) {
  43. p.refreshPolicy = policy
  44. }
  45. func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Account) (string, error) {
  46. if account == nil {
  47. return "", errors.New("account is nil")
  48. }
  49. if account.Platform != PlatformGemini || (account.Type != AccountTypeOAuth && account.Type != AccountTypeServiceAccount) {
  50. return "", errors.New("not a gemini oauth or service account")
  51. }
  52. if account.Type == AccountTypeServiceAccount {
  53. return p.getServiceAccountAccessToken(ctx, account)
  54. }
  55. if intelligentContext(ctx) != nil {
  56. return intelligentExistingAccessToken(account)
  57. }
  58. cacheKey := GeminiTokenCacheKey(account)
  59. // 1) Try cache first.
  60. if p.tokenCache != nil {
  61. if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
  62. return token, nil
  63. }
  64. }
  65. // 2) Refresh if needed (pre-expiry skew).
  66. expiresAt := account.GetCredentialAsTime("expires_at")
  67. needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew
  68. if needsRefresh && p.refreshAPI != nil && p.executor != nil {
  69. result, err := p.refreshAPI.RefreshIfNeeded(ctx, account, p.executor, geminiTokenRefreshSkew)
  70. if err != nil {
  71. if p.refreshPolicy.OnRefreshError == ProviderRefreshErrorReturn {
  72. return "", err
  73. }
  74. } else if result.LockHeld {
  75. if p.refreshPolicy.OnLockHeld == ProviderLockHeldWaitForCache && p.tokenCache != nil {
  76. if token, cacheErr := p.tokenCache.GetAccessToken(ctx, cacheKey); cacheErr == nil && strings.TrimSpace(token) != "" {
  77. return token, nil
  78. }
  79. }
  80. slog.Debug("gemini_token_lock_held_use_old", "account_id", account.ID)
  81. } else {
  82. account = result.Account
  83. expiresAt = account.GetCredentialAsTime("expires_at")
  84. }
  85. } else if needsRefresh && p.tokenCache != nil {
  86. // Backward-compatible test path when refreshAPI is not injected.
  87. locked, lockErr := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second)
  88. if lockErr == nil && locked {
  89. defer func() { _ = p.tokenCache.ReleaseRefreshLock(ctx, cacheKey) }()
  90. } else if lockErr != nil {
  91. slog.Warn("gemini_token_lock_failed", "account_id", account.ID, "error", lockErr)
  92. }
  93. }
  94. accessToken := account.GetCredential("access_token")
  95. if strings.TrimSpace(accessToken) == "" {
  96. return "", errors.New("access_token not found in credentials")
  97. }
  98. // project_id is optional now:
  99. // - If present: use Code Assist API (requires project_id)
  100. // - If absent: use AI Studio API with OAuth token.
  101. projectID := strings.TrimSpace(account.GetCredential("project_id"))
  102. autoDetectProjectID := account.GetCredential("auto_detect_project_id") == "true"
  103. if projectID == "" && autoDetectProjectID {
  104. if p.geminiOAuthService == nil {
  105. return accessToken, nil
  106. }
  107. var proxyURL string
  108. if account.ProxyID != nil && p.geminiOAuthService.proxyRepo != nil {
  109. if proxy, err := p.geminiOAuthService.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && proxy != nil {
  110. proxyURL = proxy.URL()
  111. }
  112. }
  113. detected, tierID, err := p.geminiOAuthService.fetchProjectID(ctx, accessToken, proxyURL)
  114. if err != nil {
  115. log.Printf("[GeminiTokenProvider] Auto-detect project_id failed: %v, fallback to AI Studio API mode", err)
  116. return accessToken, nil
  117. }
  118. detected = strings.TrimSpace(detected)
  119. tierID = strings.TrimSpace(tierID)
  120. if detected != "" {
  121. if account.Credentials == nil {
  122. account.Credentials = make(map[string]any)
  123. }
  124. account.Credentials["project_id"] = detected
  125. if tierID != "" {
  126. account.Credentials["tier_id"] = tierID
  127. }
  128. _ = persistAccountCredentials(ctx, p.accountRepo, account, account.Credentials)
  129. }
  130. }
  131. // 3) Populate cache with TTL.
  132. if p.tokenCache != nil {
  133. latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
  134. if isStale && latestAccount != nil {
  135. slog.Debug("gemini_token_version_stale_use_latest", "account_id", account.ID)
  136. accessToken = latestAccount.GetCredential("access_token")
  137. if strings.TrimSpace(accessToken) == "" {
  138. return "", errors.New("access_token not found after version check")
  139. }
  140. } else {
  141. ttl := 30 * time.Minute
  142. if expiresAt != nil {
  143. until := time.Until(*expiresAt)
  144. switch {
  145. case until > geminiTokenCacheSkew:
  146. ttl = until - geminiTokenCacheSkew
  147. case until > 0:
  148. ttl = until
  149. default:
  150. ttl = time.Minute
  151. }
  152. }
  153. _ = p.tokenCache.SetAccessToken(ctx, cacheKey, accessToken, ttl)
  154. }
  155. }
  156. return accessToken, nil
  157. }
  158. func (p *GeminiTokenProvider) getServiceAccountAccessToken(ctx context.Context, account *Account) (string, error) {
  159. return getVertexServiceAccountAccessToken(ctx, p.tokenCache, account)
  160. }
  161. func GeminiTokenCacheKey(account *Account) string {
  162. if account != nil && account.Type == AccountTypeServiceAccount {
  163. if key, err := parseVertexServiceAccountKey(account); err == nil {
  164. return vertexServiceAccountCacheKey(account, key)
  165. }
  166. }
  167. projectID := strings.TrimSpace(account.GetCredential("project_id"))
  168. if projectID != "" {
  169. return "gemini:" + projectID
  170. }
  171. return "gemini:account:" + strconv.FormatInt(account.ID, 10)
  172. }