2025-12-25 06:44:18 -08:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
2025-12-25 21:24:44 -08:00
|
|
|
"log"
|
2026-01-23 10:29:52 +08:00
|
|
|
"log/slog"
|
2025-12-25 06:44:18 -08:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
geminiTokenRefreshSkew = 3 * time.Minute
|
|
|
|
|
geminiTokenCacheSkew = 5 * time.Minute
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-16 01:31:54 +08:00
|
|
|
// GeminiTokenProvider manages access_token for Gemini OAuth accounts.
|
2025-12-25 06:44:18 -08:00
|
|
|
type GeminiTokenProvider struct {
|
2025-12-25 21:52:39 -08:00
|
|
|
accountRepo AccountRepository
|
|
|
|
|
tokenCache GeminiTokenCache
|
2025-12-25 06:44:18 -08:00
|
|
|
geminiOAuthService *GeminiOAuthService
|
2026-03-16 01:31:54 +08:00
|
|
|
refreshAPI *OAuthRefreshAPI
|
|
|
|
|
executor OAuthRefreshExecutor
|
|
|
|
|
refreshPolicy ProviderRefreshPolicy
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewGeminiTokenProvider(
|
2025-12-25 21:52:39 -08:00
|
|
|
accountRepo AccountRepository,
|
|
|
|
|
tokenCache GeminiTokenCache,
|
2025-12-25 06:44:18 -08:00
|
|
|
geminiOAuthService *GeminiOAuthService,
|
|
|
|
|
) *GeminiTokenProvider {
|
|
|
|
|
return &GeminiTokenProvider{
|
|
|
|
|
accountRepo: accountRepo,
|
|
|
|
|
tokenCache: tokenCache,
|
|
|
|
|
geminiOAuthService: geminiOAuthService,
|
2026-03-16 01:31:54 +08:00
|
|
|
refreshPolicy: GeminiProviderRefreshPolicy(),
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 01:31:54 +08:00
|
|
|
// SetRefreshAPI injects unified OAuth refresh API and executor.
|
|
|
|
|
func (p *GeminiTokenProvider) SetRefreshAPI(api *OAuthRefreshAPI, executor OAuthRefreshExecutor) {
|
|
|
|
|
p.refreshAPI = api
|
|
|
|
|
p.executor = executor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetRefreshPolicy injects caller-side refresh policy.
|
|
|
|
|
func (p *GeminiTokenProvider) SetRefreshPolicy(policy ProviderRefreshPolicy) {
|
|
|
|
|
p.refreshPolicy = policy
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 22:07:55 +08:00
|
|
|
func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Account) (string, error) {
|
2025-12-25 06:44:18 -08:00
|
|
|
if account == nil {
|
|
|
|
|
return "", errors.New("account is nil")
|
|
|
|
|
}
|
2025-12-26 22:07:55 +08:00
|
|
|
if account.Platform != PlatformGemini || account.Type != AccountTypeOAuth {
|
2025-12-25 06:44:18 -08:00
|
|
|
return "", errors.New("not a gemini oauth account")
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 15:55:44 +08:00
|
|
|
cacheKey := GeminiTokenCacheKey(account)
|
2025-12-25 06:44:18 -08:00
|
|
|
|
|
|
|
|
// 1) Try cache first.
|
|
|
|
|
if p.tokenCache != nil {
|
|
|
|
|
if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
|
|
|
|
|
return token, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2) Refresh if needed (pre-expiry skew).
|
2025-12-31 16:25:45 +08:00
|
|
|
expiresAt := account.GetCredentialAsTime("expires_at")
|
2025-12-25 06:44:18 -08:00
|
|
|
needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew
|
|
|
|
|
|
2026-03-16 01:31:54 +08:00
|
|
|
if needsRefresh && p.refreshAPI != nil && p.executor != nil {
|
|
|
|
|
result, err := p.refreshAPI.RefreshIfNeeded(ctx, account, p.executor, geminiTokenRefreshSkew)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if p.refreshPolicy.OnRefreshError == ProviderRefreshErrorReturn {
|
|
|
|
|
return "", err
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
2026-03-16 01:31:54 +08:00
|
|
|
} else if result.LockHeld {
|
|
|
|
|
if p.refreshPolicy.OnLockHeld == ProviderLockHeldWaitForCache && p.tokenCache != nil {
|
|
|
|
|
if token, cacheErr := p.tokenCache.GetAccessToken(ctx, cacheKey); cacheErr == nil && strings.TrimSpace(token) != "" {
|
|
|
|
|
return token, nil
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 01:31:54 +08:00
|
|
|
slog.Debug("gemini_token_lock_held_use_old", "account_id", account.ID)
|
|
|
|
|
} else {
|
|
|
|
|
account = result.Account
|
|
|
|
|
expiresAt = account.GetCredentialAsTime("expires_at")
|
|
|
|
|
}
|
|
|
|
|
} else if needsRefresh && p.tokenCache != nil {
|
|
|
|
|
// Backward-compatible test path when refreshAPI is not injected.
|
|
|
|
|
locked, lockErr := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second)
|
|
|
|
|
if lockErr == nil && locked {
|
|
|
|
|
defer func() { _ = p.tokenCache.ReleaseRefreshLock(ctx, cacheKey) }()
|
|
|
|
|
} else if lockErr != nil {
|
|
|
|
|
slog.Warn("gemini_token_lock_failed", "account_id", account.ID, "error", lockErr)
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
accessToken := account.GetCredential("access_token")
|
|
|
|
|
if strings.TrimSpace(accessToken) == "" {
|
|
|
|
|
return "", errors.New("access_token not found in credentials")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 21:24:44 -08:00
|
|
|
// project_id is optional now:
|
2026-03-16 01:31:54 +08:00
|
|
|
// - If present: use Code Assist API (requires project_id)
|
|
|
|
|
// - If absent: use AI Studio API with OAuth token.
|
2025-12-25 21:24:44 -08:00
|
|
|
projectID := strings.TrimSpace(account.GetCredential("project_id"))
|
|
|
|
|
autoDetectProjectID := account.GetCredential("auto_detect_project_id") == "true"
|
|
|
|
|
|
|
|
|
|
if projectID == "" && autoDetectProjectID {
|
|
|
|
|
if p.geminiOAuthService == nil {
|
2026-03-16 01:31:54 +08:00
|
|
|
return accessToken, nil
|
2025-12-25 21:24:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var proxyURL string
|
|
|
|
|
if account.ProxyID != nil && p.geminiOAuthService.proxyRepo != nil {
|
|
|
|
|
if proxy, err := p.geminiOAuthService.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && proxy != nil {
|
|
|
|
|
proxyURL = proxy.URL()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: 修复 /v1/messages 间歇性 400 错误 (#18)
* fix(upstream): 修复上游格式兼容性问题
- 跳过Claude模型无signature的thinking block
- 支持custom类型工具(MCP)格式转换
- 添加ClaudeCustomToolSpec结构体支持MCP工具
- 添加Custom字段验证,跳过无效custom工具
- 在convertClaudeToolsToGeminiTools中添加schema清理
- 完整的单元测试覆盖,包含边界情况
修复: Issue 0.1 signature缺失, Issue 0.2 custom工具格式
改进: Codex审查发现的2个重要问题
测试:
- TestBuildParts_ThinkingBlockWithoutSignature: 验证thinking block处理
- TestBuildTools_CustomTypeTools: 验证custom工具转换和边界情况
- TestConvertClaudeToolsToGeminiTools_CustomType: 验证service层转换
* feat(gemini): 添加Gemini限额与TierID支持
实现PR1:Gemini限额与TierID功能
后端修改:
- GeminiTokenInfo结构体添加TierID字段
- fetchProjectID函数返回(projectID, tierID, error)
- 从LoadCodeAssist响应中提取tierID(优先IsDefault,回退到第一个非空tier)
- ExchangeCode、RefreshAccountToken、GetAccessToken函数更新以处理tierID
- BuildAccountCredentials函数保存tier_id到credentials
前端修改:
- AccountStatusIndicator组件添加tier显示
- 支持LEGACY/PRO/ULTRA等tier类型的友好显示
- 使用蓝色badge展示tier信息
技术细节:
- tierID提取逻辑:优先选择IsDefault的tier,否则选择第一个非空tier
- 所有fetchProjectID调用点已更新以处理新的返回签名
- 前端gracefully处理missing/unknown tier_id
* refactor(gemini): 优化TierID实现并添加安全验证
根据并发代码审查(code-reviewer, security-auditor, gemini, codex)的反馈进行改进:
安全改进:
- 添加validateTierID函数验证tier_id格式和长度(最大64字符)
- 限制tier_id字符集为字母数字、下划线、连字符和斜杠
- 在BuildAccountCredentials中验证tier_id后再存储
- 静默跳过无效tier_id,不阻塞账户创建
代码质量改进:
- 提取extractTierIDFromAllowedTiers辅助函数消除重复代码
- 重构fetchProjectID函数,tierID提取逻辑只执行一次
- 改进代码可读性和可维护性
审查工具:
- code-reviewer agent (a09848e)
- security-auditor agent (a9a149c)
- gemini CLI (bcc7c81)
- codex (b5d8919)
修复问题:
- HIGH: 未验证的tier_id输入
- MEDIUM: 代码重复(tierID提取逻辑重复2次)
* fix(format): 修复 gofmt 格式问题
- 修复 claude_types.go 中的字段对齐问题
- 修复 gemini_messages_compat_service.go 中的缩进问题
* fix(upstream): 修复上游格式兼容性问题 (#14)
* fix(upstream): 修复上游格式兼容性问题
- 跳过Claude模型无signature的thinking block
- 支持custom类型工具(MCP)格式转换
- 添加ClaudeCustomToolSpec结构体支持MCP工具
- 添加Custom字段验证,跳过无效custom工具
- 在convertClaudeToolsToGeminiTools中添加schema清理
- 完整的单元测试覆盖,包含边界情况
修复: Issue 0.1 signature缺失, Issue 0.2 custom工具格式
改进: Codex审查发现的2个重要问题
测试:
- TestBuildParts_ThinkingBlockWithoutSignature: 验证thinking block处理
- TestBuildTools_CustomTypeTools: 验证custom工具转换和边界情况
- TestConvertClaudeToolsToGeminiTools_CustomType: 验证service层转换
* fix(format): 修复 gofmt 格式问题
- 修复 claude_types.go 中的字段对齐问题
- 修复 gemini_messages_compat_service.go 中的缩进问题
* fix(format): 修复 claude_types.go 的 gofmt 格式问题
* feat(antigravity): 优化 thinking block 和 schema 处理
- 为 dummy thinking block 添加 ThoughtSignature
- 重构 thinking block 处理逻辑,在每个条件分支内创建 part
- 优化 excludedSchemaKeys,移除 Gemini 实际支持的字段
(minItems, maxItems, minimum, maximum, additionalProperties, format)
- 添加详细注释说明 Gemini API 支持的 schema 字段
* fix(antigravity): 增强 schema 清理的安全性
基于 Codex review 建议:
- 添加 format 字段白名单过滤,只保留 Gemini 支持的 date-time/date/time
- 补充更多不支持的 schema 关键字到黑名单:
* 组合 schema: oneOf, anyOf, allOf, not, if/then/else
* 对象验证: minProperties, maxProperties, patternProperties 等
* 定义引用: $defs, definitions
- 避免不支持的 schema 字段导致 Gemini API 校验失败
* fix(lint): 修复 gemini_messages_compat_service 空分支警告
- 在 cleanToolSchema 的 if 语句中添加 continue
- 移除重复的注释
* fix(antigravity): 移除 minItems/maxItems 以兼容 Claude API
- 将 minItems 和 maxItems 添加到 schema 黑名单
- Claude API (Vertex AI) 不支持这些数组验证字段
- 添加调试日志记录工具 schema 转换过程
- 修复 tools.14.custom.input_schema 验证错误
* fix(antigravity): 修复 additionalProperties schema 对象问题
- 将 additionalProperties 的 schema 对象转换为布尔值 true
- Claude API 只支持 additionalProperties: false,不支持 schema 对象
- 修复 tools.14.custom.input_schema 验证错误
- 参考 Claude 官方文档的 JSON Schema 限制
* fix(antigravity): 修复 Claude 模型 thinking 块兼容性问题
- 完全跳过 Claude 模型的 thinking 块以避免 signature 验证失败
- 只在 Gemini 模型中使用 dummy thought signature
- 修改 additionalProperties 默认值为 false(更安全)
- 添加调试日志以便排查问题
* fix(upstream): 修复跨模型切换时的 dummy signature 问题
基于 Codex review 和用户场景分析的修复:
1. 问题场景
- Gemini (thinking) → Claude (thinking) 切换时
- Gemini 返回的 thinking 块使用 dummy signature
- Claude API 会拒绝 dummy signature,导致 400 错误
2. 修复内容
- request_transformer.go:262: 跳过 dummy signature
- 只保留真实的 Claude signature
- 支持频繁的跨模型切换
3. 其他修复(基于 Codex review)
- gateway_service.go:691: 修复 io.ReadAll 错误处理
- gateway_service.go:687: 条件日志(尊重 LogUpstreamErrorBody 配置)
- gateway_service.go:915: 收紧 400 failover 启发式
- request_transformer.go:188: 移除签名成功日志
4. 新增功能(默认关闭)
- 阶段 1: 上游错误日志(GATEWAY_LOG_UPSTREAM_ERROR_BODY)
- 阶段 2: Antigravity thinking 修复
- 阶段 3: API-key beta 注入(GATEWAY_INJECT_BETA_FOR_APIKEY)
- 阶段 3: 智能 400 failover(GATEWAY_FAILOVER_ON_400)
测试:所有测试通过
* fix(lint): 修复 golangci-lint 问题
- 应用 De Morgan 定律简化条件判断
- 修复 gofmt 格式问题
- 移除未使用的 min 函数
2026-01-01 04:21:18 +08:00
|
|
|
detected, tierID, err := p.geminiOAuthService.fetchProjectID(ctx, accessToken, proxyURL)
|
2025-12-25 21:24:44 -08:00
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("[GeminiTokenProvider] Auto-detect project_id failed: %v, fallback to AI Studio API mode", err)
|
|
|
|
|
return accessToken, nil
|
|
|
|
|
}
|
|
|
|
|
detected = strings.TrimSpace(detected)
|
2026-01-01 00:40:51 +08:00
|
|
|
tierID = strings.TrimSpace(tierID)
|
2025-12-25 21:24:44 -08:00
|
|
|
if detected != "" {
|
|
|
|
|
if account.Credentials == nil {
|
2025-12-26 22:07:55 +08:00
|
|
|
account.Credentials = make(map[string]any)
|
2025-12-25 21:24:44 -08:00
|
|
|
}
|
|
|
|
|
account.Credentials["project_id"] = detected
|
fix: 修复 /v1/messages 间歇性 400 错误 (#18)
* fix(upstream): 修复上游格式兼容性问题
- 跳过Claude模型无signature的thinking block
- 支持custom类型工具(MCP)格式转换
- 添加ClaudeCustomToolSpec结构体支持MCP工具
- 添加Custom字段验证,跳过无效custom工具
- 在convertClaudeToolsToGeminiTools中添加schema清理
- 完整的单元测试覆盖,包含边界情况
修复: Issue 0.1 signature缺失, Issue 0.2 custom工具格式
改进: Codex审查发现的2个重要问题
测试:
- TestBuildParts_ThinkingBlockWithoutSignature: 验证thinking block处理
- TestBuildTools_CustomTypeTools: 验证custom工具转换和边界情况
- TestConvertClaudeToolsToGeminiTools_CustomType: 验证service层转换
* feat(gemini): 添加Gemini限额与TierID支持
实现PR1:Gemini限额与TierID功能
后端修改:
- GeminiTokenInfo结构体添加TierID字段
- fetchProjectID函数返回(projectID, tierID, error)
- 从LoadCodeAssist响应中提取tierID(优先IsDefault,回退到第一个非空tier)
- ExchangeCode、RefreshAccountToken、GetAccessToken函数更新以处理tierID
- BuildAccountCredentials函数保存tier_id到credentials
前端修改:
- AccountStatusIndicator组件添加tier显示
- 支持LEGACY/PRO/ULTRA等tier类型的友好显示
- 使用蓝色badge展示tier信息
技术细节:
- tierID提取逻辑:优先选择IsDefault的tier,否则选择第一个非空tier
- 所有fetchProjectID调用点已更新以处理新的返回签名
- 前端gracefully处理missing/unknown tier_id
* refactor(gemini): 优化TierID实现并添加安全验证
根据并发代码审查(code-reviewer, security-auditor, gemini, codex)的反馈进行改进:
安全改进:
- 添加validateTierID函数验证tier_id格式和长度(最大64字符)
- 限制tier_id字符集为字母数字、下划线、连字符和斜杠
- 在BuildAccountCredentials中验证tier_id后再存储
- 静默跳过无效tier_id,不阻塞账户创建
代码质量改进:
- 提取extractTierIDFromAllowedTiers辅助函数消除重复代码
- 重构fetchProjectID函数,tierID提取逻辑只执行一次
- 改进代码可读性和可维护性
审查工具:
- code-reviewer agent (a09848e)
- security-auditor agent (a9a149c)
- gemini CLI (bcc7c81)
- codex (b5d8919)
修复问题:
- HIGH: 未验证的tier_id输入
- MEDIUM: 代码重复(tierID提取逻辑重复2次)
* fix(format): 修复 gofmt 格式问题
- 修复 claude_types.go 中的字段对齐问题
- 修复 gemini_messages_compat_service.go 中的缩进问题
* fix(upstream): 修复上游格式兼容性问题 (#14)
* fix(upstream): 修复上游格式兼容性问题
- 跳过Claude模型无signature的thinking block
- 支持custom类型工具(MCP)格式转换
- 添加ClaudeCustomToolSpec结构体支持MCP工具
- 添加Custom字段验证,跳过无效custom工具
- 在convertClaudeToolsToGeminiTools中添加schema清理
- 完整的单元测试覆盖,包含边界情况
修复: Issue 0.1 signature缺失, Issue 0.2 custom工具格式
改进: Codex审查发现的2个重要问题
测试:
- TestBuildParts_ThinkingBlockWithoutSignature: 验证thinking block处理
- TestBuildTools_CustomTypeTools: 验证custom工具转换和边界情况
- TestConvertClaudeToolsToGeminiTools_CustomType: 验证service层转换
* fix(format): 修复 gofmt 格式问题
- 修复 claude_types.go 中的字段对齐问题
- 修复 gemini_messages_compat_service.go 中的缩进问题
* fix(format): 修复 claude_types.go 的 gofmt 格式问题
* feat(antigravity): 优化 thinking block 和 schema 处理
- 为 dummy thinking block 添加 ThoughtSignature
- 重构 thinking block 处理逻辑,在每个条件分支内创建 part
- 优化 excludedSchemaKeys,移除 Gemini 实际支持的字段
(minItems, maxItems, minimum, maximum, additionalProperties, format)
- 添加详细注释说明 Gemini API 支持的 schema 字段
* fix(antigravity): 增强 schema 清理的安全性
基于 Codex review 建议:
- 添加 format 字段白名单过滤,只保留 Gemini 支持的 date-time/date/time
- 补充更多不支持的 schema 关键字到黑名单:
* 组合 schema: oneOf, anyOf, allOf, not, if/then/else
* 对象验证: minProperties, maxProperties, patternProperties 等
* 定义引用: $defs, definitions
- 避免不支持的 schema 字段导致 Gemini API 校验失败
* fix(lint): 修复 gemini_messages_compat_service 空分支警告
- 在 cleanToolSchema 的 if 语句中添加 continue
- 移除重复的注释
* fix(antigravity): 移除 minItems/maxItems 以兼容 Claude API
- 将 minItems 和 maxItems 添加到 schema 黑名单
- Claude API (Vertex AI) 不支持这些数组验证字段
- 添加调试日志记录工具 schema 转换过程
- 修复 tools.14.custom.input_schema 验证错误
* fix(antigravity): 修复 additionalProperties schema 对象问题
- 将 additionalProperties 的 schema 对象转换为布尔值 true
- Claude API 只支持 additionalProperties: false,不支持 schema 对象
- 修复 tools.14.custom.input_schema 验证错误
- 参考 Claude 官方文档的 JSON Schema 限制
* fix(antigravity): 修复 Claude 模型 thinking 块兼容性问题
- 完全跳过 Claude 模型的 thinking 块以避免 signature 验证失败
- 只在 Gemini 模型中使用 dummy thought signature
- 修改 additionalProperties 默认值为 false(更安全)
- 添加调试日志以便排查问题
* fix(upstream): 修复跨模型切换时的 dummy signature 问题
基于 Codex review 和用户场景分析的修复:
1. 问题场景
- Gemini (thinking) → Claude (thinking) 切换时
- Gemini 返回的 thinking 块使用 dummy signature
- Claude API 会拒绝 dummy signature,导致 400 错误
2. 修复内容
- request_transformer.go:262: 跳过 dummy signature
- 只保留真实的 Claude signature
- 支持频繁的跨模型切换
3. 其他修复(基于 Codex review)
- gateway_service.go:691: 修复 io.ReadAll 错误处理
- gateway_service.go:687: 条件日志(尊重 LogUpstreamErrorBody 配置)
- gateway_service.go:915: 收紧 400 failover 启发式
- request_transformer.go:188: 移除签名成功日志
4. 新增功能(默认关闭)
- 阶段 1: 上游错误日志(GATEWAY_LOG_UPSTREAM_ERROR_BODY)
- 阶段 2: Antigravity thinking 修复
- 阶段 3: API-key beta 注入(GATEWAY_INJECT_BETA_FOR_APIKEY)
- 阶段 3: 智能 400 failover(GATEWAY_FAILOVER_ON_400)
测试:所有测试通过
* fix(lint): 修复 golangci-lint 问题
- 应用 De Morgan 定律简化条件判断
- 修复 gofmt 格式问题
- 移除未使用的 min 函数
2026-01-01 04:21:18 +08:00
|
|
|
if tierID != "" {
|
|
|
|
|
account.Credentials["tier_id"] = tierID
|
|
|
|
|
}
|
2025-12-25 21:24:44 -08:00
|
|
|
_ = p.accountRepo.Update(ctx, account)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 01:31:54 +08:00
|
|
|
// 3) Populate cache with TTL.
|
2026-01-23 10:29:52 +08:00
|
|
|
if p.tokenCache != nil {
|
|
|
|
|
latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
|
|
|
|
|
if isStale && latestAccount != nil {
|
|
|
|
|
slog.Debug("gemini_token_version_stale_use_latest", "account_id", account.ID)
|
|
|
|
|
accessToken = latestAccount.GetCredential("access_token")
|
|
|
|
|
if strings.TrimSpace(accessToken) == "" {
|
|
|
|
|
return "", errors.New("access_token not found after version check")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ttl := 30 * time.Minute
|
|
|
|
|
if expiresAt != nil {
|
|
|
|
|
until := time.Until(*expiresAt)
|
|
|
|
|
switch {
|
|
|
|
|
case until > geminiTokenCacheSkew:
|
|
|
|
|
ttl = until - geminiTokenCacheSkew
|
|
|
|
|
case until > 0:
|
|
|
|
|
ttl = until
|
|
|
|
|
default:
|
|
|
|
|
ttl = time.Minute
|
|
|
|
|
}
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
2026-01-23 10:29:52 +08:00
|
|
|
_ = p.tokenCache.SetAccessToken(ctx, cacheKey, accessToken, ttl)
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return accessToken, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 15:55:44 +08:00
|
|
|
func GeminiTokenCacheKey(account *Account) string {
|
2025-12-25 06:44:18 -08:00
|
|
|
projectID := strings.TrimSpace(account.GetCredential("project_id"))
|
|
|
|
|
if projectID != "" {
|
2026-01-15 19:08:07 +08:00
|
|
|
return "gemini:" + projectID
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|
2026-01-15 19:08:07 +08:00
|
|
|
return "gemini:account:" + strconv.FormatInt(account.ID, 10)
|
2025-12-25 06:44:18 -08:00
|
|
|
}
|