feat: add official subscription quota display for Gemini

- Read Gemini OAuth credentials from macOS Keychain (gemini-cli-oauth)
  or legacy file (~/.gemini/oauth_creds.json)
- Auto-refresh expired access tokens using refresh_token (Google OAuth
  tokens expire in ~1h, unlike Claude/Codex)
- Two-step API: loadCodeAssist for project ID, then retrieveUserQuota
  for per-model quota buckets
- Classify models into Pro/Flash/Flash Lite categories, show min
  remaining fraction as utilization percentage
- Extend isOfficialProvider() for Gemini (no API key + no base URL)
- Parameterize expiredHint i18n key with tool name for all three apps
This commit is contained in:
Jason
2026-04-04 18:59:50 +08:00
parent 0200fe79ae
commit 98230c3970
6 changed files with 528 additions and 5 deletions
+5 -1
View File
@@ -16,6 +16,10 @@ const TIER_I18N_KEYS: Record<string, string> = {
seven_day: "subscription.sevenDay",
seven_day_opus: "subscription.sevenDayOpus",
seven_day_sonnet: "subscription.sevenDaySonnet",
// Gemini 模型分类
gemini_pro: "subscription.geminiPro",
gemini_flash: "subscription.geminiFlash",
gemini_flash_lite: "subscription.geminiFlashLite",
};
/** 根据使用百分比返回颜色 class */
@@ -123,7 +127,7 @@ const SubscriptionQuotaFooter: React.FC<SubscriptionQuotaFooterProps> = ({
<div>
<span className="font-medium">{t("subscription.expired")}</span>
<span className="ml-2 text-amber-500/70 dark:text-amber-400/70">
{t("subscription.expiredHint")}
{t("subscription.expiredHint", { tool: appId })}
</span>
</div>
</div>
@@ -68,6 +68,15 @@ function isOfficialProvider(provider: Provider, appId: AppId): boolean {
const apiKey = config?.auth?.OPENAI_API_KEY;
return !apiKey || (typeof apiKey === "string" && apiKey.trim() === "");
}
if (appId === "gemini") {
// 无 GEMINI_API_KEY 且无 GOOGLE_GEMINI_BASE_URL → Google OAuth 官方模式
const apiKey = config?.env?.GEMINI_API_KEY;
const baseUrl = config?.env?.GOOGLE_GEMINI_BASE_URL;
return (
(!apiKey || (typeof apiKey === "string" && apiKey.trim() === "")) &&
(!baseUrl || (typeof baseUrl === "string" && baseUrl.trim() === ""))
);
}
return false;
}
+4 -1
View File
@@ -2283,11 +2283,14 @@
"sevenDay": "7-Day",
"sevenDayOpus": "7-Day (Opus)",
"sevenDaySonnet": "7-Day (Sonnet)",
"geminiPro": "Pro",
"geminiFlash": "Flash",
"geminiFlashLite": "Flash Lite",
"utilization": "{{value}}%",
"resetsIn": "Resets in {{time}}",
"extraUsage": "Extra Usage",
"expired": "Session expired",
"expiredHint": "Run the claude command to refresh your login",
"expiredHint": "Run the {{tool}} command to refresh your login",
"queryFailed": "Query failed",
"refresh": "Refresh"
}
+4 -1
View File
@@ -2283,11 +2283,14 @@
"sevenDay": "7日間",
"sevenDayOpus": "7日間(Opus)",
"sevenDaySonnet": "7日間(Sonnet)",
"geminiPro": "Pro",
"geminiFlash": "Flash",
"geminiFlashLite": "Flash Lite",
"utilization": "{{value}}%",
"resetsIn": "{{time}}後にリセット",
"extraUsage": "超過使用量",
"expired": "セッション期限切れ",
"expiredHint": "claudeコマンドを実行してログインを更新してください",
"expiredHint": "{{tool}}コマンドを実行してログインを更新してください",
"queryFailed": "クエリに失敗しました",
"refresh": "更新"
}
+4 -1
View File
@@ -2283,11 +2283,14 @@
"sevenDay": "7天",
"sevenDayOpus": "7天(Opus)",
"sevenDaySonnet": "7天(Sonnet)",
"geminiPro": "Pro",
"geminiFlash": "Flash",
"geminiFlashLite": "Flash Lite",
"utilization": "{{value}}%",
"resetsIn": "{{time}}后重置",
"extraUsage": "超额用量",
"expired": "会话已过期",
"expiredHint": "请运行 claude 命令刷新登录",
"expiredHint": "请运行 {{tool}} 命令刷新登录",
"queryFailed": "查询失败",
"refresh": "刷新"
}