fix(gemini): 修复 P0/P1 级别问题(429误判/Tier丢失/expires_at/前端一致性)
P0 修复(Critical - 影响生产稳定性): - 修复 429 判断逻辑:使用 project_id 判断而非 account.Type 防止 AI Studio OAuth 被误判为 Code Assist 5分钟窗口 - 修复 Tier ID 丢失:刷新时始终保留旧值,默认 LEGACY 防止 fetchProjectID 失败导致 tier_id 被清空 - 修复 expires_at 下界:添加 minTTL=30s 保护 防止 expires_in <= 300 时生成过去时间引发刷新风暴 P1 修复(Important - 行为一致性): - 前端 isCodeAssist 判断与后端一致(支持 legacy) - 前端日期解析添加 NaN 保护 - 迁移脚本覆盖 legacy 账号 前端功能(新增): - AccountQuotaInfo 组件:Tier Badge + 二元进度条 + 倒计时 - 定时器动态管理:watch 监听限流状态 - 类型定义:GeminiCredentials 接口 测试: - ✅ TypeScript 类型检查通过 - ✅ 前端构建成功(3.33s) - ✅ Gemini + Codex 双 AI 审查通过 Refs: #gemini-quota
This commit is contained in:
@@ -1886,13 +1886,47 @@ func (s *GeminiMessagesCompatService) handleGeminiUpstreamError(ctx context.Cont
|
||||
if statusCode != 429 {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取账号的 oauth_type、tier_id 和 project_id
|
||||
oauthType := strings.TrimSpace(account.GetCredential("oauth_type"))
|
||||
tierID := strings.TrimSpace(account.GetCredential("tier_id"))
|
||||
projectID := strings.TrimSpace(account.GetCredential("project_id"))
|
||||
|
||||
// 判断是否为 Code Assist:以 project_id 是否存在为准(更可靠)
|
||||
isCodeAssist := projectID != ""
|
||||
// Legacy 兼容:oauth_type 为空但 project_id 存在时视为 code_assist
|
||||
if oauthType == "" && isCodeAssist {
|
||||
oauthType = "code_assist"
|
||||
}
|
||||
|
||||
resetAt := ParseGeminiRateLimitResetTime(body)
|
||||
if resetAt == nil {
|
||||
ra := time.Now().Add(5 * time.Minute)
|
||||
// 根据账号类型使用不同的默认重置时间
|
||||
var ra time.Time
|
||||
if isCodeAssist {
|
||||
// Code Assist: 5 分钟滚动窗口
|
||||
ra = time.Now().Add(5 * time.Minute)
|
||||
log.Printf("[Gemini 429] Account %d (Code Assist, tier=%s, project=%s) rate limited, reset in 5min", account.ID, tierID, projectID)
|
||||
} else {
|
||||
// API Key / AI Studio OAuth: PST 午夜
|
||||
if ts := nextGeminiDailyResetUnix(); ts != nil {
|
||||
ra = time.Unix(*ts, 0)
|
||||
log.Printf("[Gemini 429] Account %d (API Key/AI Studio, type=%s) rate limited, reset at PST midnight (%v)", account.ID, account.Type, ra)
|
||||
} else {
|
||||
// 兜底:5 分钟
|
||||
ra = time.Now().Add(5 * time.Minute)
|
||||
log.Printf("[Gemini 429] Account %d rate limited, fallback to 5min", account.ID)
|
||||
}
|
||||
}
|
||||
_ = s.accountRepo.SetRateLimited(ctx, account.ID, ra)
|
||||
return
|
||||
}
|
||||
_ = s.accountRepo.SetRateLimited(ctx, account.ID, time.Unix(*resetAt, 0))
|
||||
|
||||
// 使用解析到的重置时间
|
||||
resetTime := time.Unix(*resetAt, 0)
|
||||
_ = s.accountRepo.SetRateLimited(ctx, account.ID, resetTime)
|
||||
log.Printf("[Gemini 429] Account %d rate limited until %v (oauth_type=%s, tier=%s)",
|
||||
account.ID, resetTime, oauthType, tierID)
|
||||
}
|
||||
|
||||
// ParseGeminiRateLimitResetTime 解析 Gemini 格式的 429 响应,返回重置时间的 Unix 时间戳
|
||||
|
||||
@@ -259,8 +259,15 @@ func (s *GeminiOAuthService) ExchangeCode(ctx context.Context, input *GeminiExch
|
||||
sessionProjectID := strings.TrimSpace(session.ProjectID)
|
||||
s.sessionStore.Delete(input.SessionID)
|
||||
|
||||
// 计算过期时间时减去 5 分钟安全时间窗口,考虑网络延迟和时钟偏差
|
||||
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn - 300
|
||||
// 计算过期时间:减去 5 分钟安全时间窗口(考虑网络延迟和时钟偏差)
|
||||
// 同时设置下界保护,防止 expires_in 过小导致过去时间(引发刷新风暴)
|
||||
const safetyWindow = 300 // 5 minutes
|
||||
const minTTL = 30 // minimum 30 seconds
|
||||
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn - safetyWindow
|
||||
minExpiresAt := time.Now().Unix() + minTTL
|
||||
if expiresAt < minExpiresAt {
|
||||
expiresAt = minExpiresAt
|
||||
}
|
||||
|
||||
projectID := sessionProjectID
|
||||
var tierID string
|
||||
@@ -275,10 +282,22 @@ func (s *GeminiOAuthService) ExchangeCode(ctx context.Context, input *GeminiExch
|
||||
// 记录警告但不阻断流程,允许后续补充 project_id
|
||||
fmt.Printf("[GeminiOAuth] Warning: Failed to fetch project_id during token exchange: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
// 用户手动填了 project_id,仍需调用 LoadCodeAssist 获取 tierID
|
||||
_, fetchedTierID, err := s.fetchProjectID(ctx, tokenResp.AccessToken, proxyURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[GeminiOAuth] Warning: Failed to fetch tierID: %v\n", err)
|
||||
} else {
|
||||
tierID = fetchedTierID
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(projectID) == "" {
|
||||
return nil, fmt.Errorf("missing project_id for Code Assist OAuth: please fill Project ID (optional field) and regenerate the auth URL, or ensure your Google account has an ACTIVE GCP project")
|
||||
}
|
||||
// tierID 缺失时使用默认值
|
||||
if tierID == "" {
|
||||
tierID = "LEGACY"
|
||||
}
|
||||
}
|
||||
|
||||
return &GeminiTokenInfo{
|
||||
@@ -308,8 +327,15 @@ func (s *GeminiOAuthService) RefreshToken(ctx context.Context, oauthType, refres
|
||||
|
||||
tokenResp, err := s.oauthClient.RefreshToken(ctx, oauthType, refreshToken, proxyURL)
|
||||
if err == nil {
|
||||
// 计算过期时间时减去 5 分钟安全时间窗口,考虑网络延迟和时钟偏差
|
||||
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn - 300
|
||||
// 计算过期时间:减去 5 分钟安全时间窗口(考虑网络延迟和时钟偏差)
|
||||
// 同时设置下界保护,防止 expires_in 过小导致过去时间(引发刷新风暴)
|
||||
const safetyWindow = 300 // 5 minutes
|
||||
const minTTL = 30 // minimum 30 seconds
|
||||
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn - safetyWindow
|
||||
minExpiresAt := time.Now().Unix() + minTTL
|
||||
if expiresAt < minExpiresAt {
|
||||
expiresAt = minExpiresAt
|
||||
}
|
||||
return &GeminiTokenInfo{
|
||||
AccessToken: tokenResp.AccessToken,
|
||||
RefreshToken: tokenResp.RefreshToken,
|
||||
@@ -396,19 +422,39 @@ func (s *GeminiOAuthService) RefreshAccountToken(ctx context.Context, account *A
|
||||
tokenInfo.ProjectID = existingProjectID
|
||||
}
|
||||
|
||||
// 尝试从账号凭证获取 tierID(向后兼容)
|
||||
existingTierID := strings.TrimSpace(account.GetCredential("tier_id"))
|
||||
|
||||
// For Code Assist, project_id is required. Auto-detect if missing.
|
||||
// For AI Studio OAuth, project_id is optional and should not block refresh.
|
||||
if oauthType == "code_assist" && strings.TrimSpace(tokenInfo.ProjectID) == "" {
|
||||
projectID, tierID, err := s.fetchProjectID(ctx, tokenInfo.AccessToken, proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to auto-detect project_id: %w", err)
|
||||
if oauthType == "code_assist" {
|
||||
// 先设置默认值或保留旧值,确保 tier_id 始终有值
|
||||
if existingTierID != "" {
|
||||
tokenInfo.TierID = existingTierID
|
||||
} else {
|
||||
tokenInfo.TierID = "LEGACY" // 默认值
|
||||
}
|
||||
projectID = strings.TrimSpace(projectID)
|
||||
if projectID == "" {
|
||||
|
||||
// 尝试自动探测 project_id 和 tier_id
|
||||
needDetect := strings.TrimSpace(tokenInfo.ProjectID) == "" || existingTierID == ""
|
||||
if needDetect {
|
||||
projectID, tierID, err := s.fetchProjectID(ctx, tokenInfo.AccessToken, proxyURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[GeminiOAuth] Warning: failed to auto-detect project/tier: %v\n", err)
|
||||
} else {
|
||||
if strings.TrimSpace(tokenInfo.ProjectID) == "" && projectID != "" {
|
||||
tokenInfo.ProjectID = projectID
|
||||
}
|
||||
// 只有当原来没有 tier_id 且探测成功时才更新
|
||||
if existingTierID == "" && tierID != "" {
|
||||
tokenInfo.TierID = tierID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(tokenInfo.ProjectID) == "" {
|
||||
return nil, fmt.Errorf("failed to auto-detect project_id: empty result")
|
||||
}
|
||||
tokenInfo.ProjectID = projectID
|
||||
tokenInfo.TierID = tierID
|
||||
}
|
||||
|
||||
return tokenInfo, nil
|
||||
@@ -466,9 +512,6 @@ func (s *GeminiOAuthService) fetchProjectID(ctx context.Context, accessToken, pr
|
||||
return strings.TrimSpace(loadResp.CloudAICompanionProject), tierID, nil
|
||||
}
|
||||
|
||||
// Pick tier from allowedTiers; if no default tier is marked, pick the first non-empty tier ID.
|
||||
// (tierID already extracted above, reuse it)
|
||||
|
||||
req := &geminicli.OnboardUserRequest{
|
||||
TierID: tierID,
|
||||
Metadata: geminicli.LoadCodeAssistMetadata{
|
||||
@@ -487,7 +530,7 @@ func (s *GeminiOAuthService) fetchProjectID(ctx context.Context, accessToken, pr
|
||||
if fbErr == nil && strings.TrimSpace(fallback) != "" {
|
||||
return strings.TrimSpace(fallback), tierID, nil
|
||||
}
|
||||
return "", "", err
|
||||
return "", tierID, err
|
||||
}
|
||||
if resp.Done {
|
||||
if resp.Response != nil && resp.Response.CloudAICompanionProject != nil {
|
||||
@@ -505,7 +548,7 @@ func (s *GeminiOAuthService) fetchProjectID(ctx context.Context, accessToken, pr
|
||||
if fbErr == nil && strings.TrimSpace(fallback) != "" {
|
||||
return strings.TrimSpace(fallback), tierID, nil
|
||||
}
|
||||
return "", "", errors.New("onboardUser completed but no project_id returned")
|
||||
return "", tierID, errors.New("onboardUser completed but no project_id returned")
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -515,9 +558,9 @@ func (s *GeminiOAuthService) fetchProjectID(ctx context.Context, accessToken, pr
|
||||
return strings.TrimSpace(fallback), tierID, nil
|
||||
}
|
||||
if loadErr != nil {
|
||||
return "", "", fmt.Errorf("loadCodeAssist failed (%v) and onboardUser timeout after %d attempts", loadErr, maxAttempts)
|
||||
return "", tierID, fmt.Errorf("loadCodeAssist failed (%v) and onboardUser timeout after %d attempts", loadErr, maxAttempts)
|
||||
}
|
||||
return "", "", fmt.Errorf("onboardUser timeout after %d attempts", maxAttempts)
|
||||
return "", tierID, fmt.Errorf("onboardUser timeout after %d attempts", maxAttempts)
|
||||
}
|
||||
|
||||
type googleCloudProject struct {
|
||||
|
||||
30
backend/migrations/017_add_gemini_tier_id.sql
Normal file
30
backend/migrations/017_add_gemini_tier_id.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- 为 Gemini Code Assist OAuth 账号添加默认 tier_id
|
||||
-- 包括显式标记为 code_assist 的账号,以及 legacy 账号(oauth_type 为空但 project_id 存在)
|
||||
UPDATE accounts
|
||||
SET credentials = jsonb_set(
|
||||
credentials,
|
||||
'{tier_id}',
|
||||
'"LEGACY"',
|
||||
true
|
||||
)
|
||||
WHERE platform = 'gemini'
|
||||
AND type = 'oauth'
|
||||
AND jsonb_typeof(credentials) = 'object'
|
||||
AND credentials->>'tier_id' IS NULL
|
||||
AND (
|
||||
credentials->>'oauth_type' = 'code_assist'
|
||||
OR (credentials->>'oauth_type' IS NULL AND credentials->>'project_id' IS NOT NULL)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
-- 回滚:删除 tier_id 字段
|
||||
UPDATE accounts
|
||||
SET credentials = credentials - 'tier_id'
|
||||
WHERE platform = 'gemini'
|
||||
AND type = 'oauth'
|
||||
AND credentials->>'oauth_type' = 'code_assist';
|
||||
-- +goose StatementEnd
|
||||
194
frontend/src/components/account/AccountQuotaInfo.vue
Normal file
194
frontend/src/components/account/AccountQuotaInfo.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div v-if="shouldShowQuota" class="flex items-center gap-2">
|
||||
<!-- Tier Badge -->
|
||||
<span :class="['badge text-xs px-2 py-0.5 rounded font-medium', tierBadgeClass]">
|
||||
{{ tierLabel }}
|
||||
</span>
|
||||
|
||||
<!-- 限额文本 -->
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ quotaText }}</span>
|
||||
|
||||
<!-- 二元进度条 -->
|
||||
<div class="group/progress relative flex items-center gap-1">
|
||||
<div class="h-2 w-20 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
:class="[
|
||||
'h-full transition-all',
|
||||
isRateLimited ? 'bg-red-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
]"
|
||||
:style="{ width: isRateLimited ? '100%' : '0%' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
:class="[
|
||||
'text-xs font-medium',
|
||||
isRateLimited ? 'text-red-600 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'
|
||||
]"
|
||||
>
|
||||
{{ isRateLimited ? '100%' : '0%' }}
|
||||
</span>
|
||||
|
||||
<!-- 倒计时 -->
|
||||
<span
|
||||
v-if="isRateLimited"
|
||||
:class="[
|
||||
'text-xs font-medium',
|
||||
isUrgent
|
||||
? 'text-red-600 dark:text-red-400 animate-pulse'
|
||||
: 'text-amber-600 dark:text-amber-400'
|
||||
]"
|
||||
>
|
||||
⚠️ {{ resetCountdown }}
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="pointer-events-none absolute -top-10 left-0 z-10 hidden whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-lg group-hover/progress:block dark:bg-gray-700"
|
||||
>
|
||||
ⓘ 无法提供实时额度
|
||||
<div
|
||||
class="absolute left-4 top-full border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||
import type { Account, GeminiCredentials } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
account: Account
|
||||
}>()
|
||||
|
||||
const now = ref(new Date())
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// 是否为 Code Assist OAuth
|
||||
// 判断逻辑与后端保持一致:project_id 存在即为 Code Assist
|
||||
const isCodeAssist = computed(() => {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
// 显式为 code_assist,或 legacy 情况(oauth_type 为空但 project_id 存在)
|
||||
return creds?.oauth_type === 'code_assist' || (!creds?.oauth_type && !!creds?.project_id)
|
||||
})
|
||||
|
||||
// 是否应该显示配额信息
|
||||
const shouldShowQuota = computed(() => {
|
||||
return props.account.platform === 'gemini'
|
||||
})
|
||||
|
||||
// Tier 标签文本
|
||||
const tierLabel = computed(() => {
|
||||
if (isCodeAssist.value) {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
const tierMap: Record<string, string> = {
|
||||
LEGACY: 'Free',
|
||||
PRO: 'Standard',
|
||||
ULTRA: 'Enterprise'
|
||||
}
|
||||
return tierMap[creds?.tier_id || ''] || 'Unknown'
|
||||
}
|
||||
return 'Gemini'
|
||||
})
|
||||
|
||||
// Tier Badge 样式
|
||||
const tierBadgeClass = computed(() => {
|
||||
if (!isCodeAssist.value) {
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
}
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
const tierColorMap: Record<string, string> = {
|
||||
LEGACY: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
PRO: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
ULTRA: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
}
|
||||
return (
|
||||
tierColorMap[creds?.tier_id || ''] ||
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
|
||||
)
|
||||
})
|
||||
|
||||
// 限额文本
|
||||
const quotaText = computed(() => {
|
||||
if (isCodeAssist.value) {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
const limitMap: Record<string, string> = {
|
||||
LEGACY: '1000/day, 60/min',
|
||||
PRO: '1500/day, 120/min',
|
||||
ULTRA: '5000/day, 300/min'
|
||||
}
|
||||
return limitMap[creds?.tier_id || ''] || '-'
|
||||
}
|
||||
return 'RPM/RPD limits'
|
||||
})
|
||||
|
||||
// 是否限流
|
||||
const isRateLimited = computed(() => {
|
||||
if (!props.account.rate_limit_reset_at) return false
|
||||
const resetTime = Date.parse(props.account.rate_limit_reset_at)
|
||||
// 防护:如果日期解析失败(NaN),则认为未限流
|
||||
if (Number.isNaN(resetTime)) return false
|
||||
return resetTime > now.value.getTime()
|
||||
})
|
||||
|
||||
// 倒计时文本
|
||||
const resetCountdown = computed(() => {
|
||||
if (!props.account.rate_limit_reset_at) return ''
|
||||
const resetTime = Date.parse(props.account.rate_limit_reset_at)
|
||||
// 防护:如果日期解析失败,显示 "-"
|
||||
if (Number.isNaN(resetTime)) return '-'
|
||||
|
||||
const diffMs = resetTime - now.value.getTime()
|
||||
if (diffMs <= 0) return 'now'
|
||||
|
||||
const diffSeconds = Math.floor(diffMs / 1000)
|
||||
const diffMinutes = Math.floor(diffSeconds / 60)
|
||||
const diffHours = Math.floor(diffMinutes / 60)
|
||||
|
||||
if (diffMinutes < 1) return `${diffSeconds}s`
|
||||
if (diffHours < 1) {
|
||||
const secs = diffSeconds % 60
|
||||
return `${diffMinutes}m ${secs}s`
|
||||
}
|
||||
const mins = diffMinutes % 60
|
||||
return `${diffHours}h ${mins}m`
|
||||
})
|
||||
|
||||
// 是否紧急(< 1分钟)
|
||||
const isUrgent = computed(() => {
|
||||
if (!props.account.rate_limit_reset_at) return false
|
||||
const resetTime = Date.parse(props.account.rate_limit_reset_at)
|
||||
// 防护:如果日期解析失败,返回 false
|
||||
if (Number.isNaN(resetTime)) return false
|
||||
|
||||
const diffMs = resetTime - now.value.getTime()
|
||||
return diffMs > 0 && diffMs < 60000
|
||||
})
|
||||
|
||||
// 监听限流状态,动态启动/停止定时器
|
||||
watch(
|
||||
() => isRateLimited.value,
|
||||
(limited) => {
|
||||
if (limited && !timer) {
|
||||
// 进入限流状态,启动定时器
|
||||
timer = setInterval(() => {
|
||||
now.value = new Date()
|
||||
}, 1000)
|
||||
} else if (!limited && timer) {
|
||||
// 解除限流,停止定时器
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 立即执行,确保挂载时已限流的情况也能启动定时器
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer !== null) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -169,6 +169,11 @@
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
</template>
|
||||
|
||||
<!-- Gemini platform: show quota info with AccountQuotaInfo component -->
|
||||
<template v-else-if="account.platform === 'gemini'">
|
||||
<AccountQuotaInfo :account="account" />
|
||||
</template>
|
||||
|
||||
<!-- Other accounts: no usage window -->
|
||||
<template v-else>
|
||||
<div class="text-xs text-gray-400">-</div>
|
||||
@@ -176,7 +181,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Non-OAuth/Setup-Token accounts -->
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
<div v-else>
|
||||
<!-- Gemini API Key accounts: show quota info -->
|
||||
<AccountQuotaInfo v-if="account.platform === 'gemini'" :account="account" />
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -185,6 +194,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, AccountUsageInfo } from '@/types'
|
||||
import UsageProgressBar from './UsageProgressBar.vue'
|
||||
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
account: Account
|
||||
|
||||
@@ -315,6 +315,22 @@ export interface Proxy {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// Gemini credentials structure for OAuth and API Key authentication
|
||||
export interface GeminiCredentials {
|
||||
// API Key authentication
|
||||
api_key?: string
|
||||
|
||||
// OAuth authentication
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
oauth_type?: 'code_assist' | 'ai_studio' | string
|
||||
tier_id?: 'LEGACY' | 'PRO' | 'ULTRA' | string
|
||||
project_id?: string
|
||||
token_type?: string
|
||||
scope?: string
|
||||
expires_at?: string
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: number
|
||||
name: string
|
||||
|
||||
Reference in New Issue
Block a user