diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 3ddf21323..da27db797 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -10,6 +10,7 @@
"core:window:allow-theme",
"core:window:allow-set-background-color",
"core:webview:allow-set-webview-background-color",
- "dialog:allow-open"
+ "dialog:allow-open",
+ "dialog:allow-save"
]
}
diff --git a/src-tauri/src/management_api.rs b/src-tauri/src/management_api.rs
index 93b85b8f8..473941ca5 100644
--- a/src-tauri/src/management_api.rs
+++ b/src-tauri/src/management_api.rs
@@ -10,7 +10,7 @@ use std::{
collections::HashMap,
error::Error,
fs,
- path::Path,
+ path::{Path, PathBuf},
process::{Command, Stdio},
sync::LazyLock,
time::Duration,
@@ -127,6 +127,36 @@ pub(crate) async fn upload_auth_file(
read_management_value(response).await
}
+#[tauri::command]
+pub(crate) fn export_auth_file(source: String, target: String) -> Result<(), String> {
+ let install_dir = core_install_dir()?;
+ let candidate = PathBuf::from(source.trim());
+ // The credentials list reports paths relative to the core install directory.
+ let source_path = if candidate.is_absolute() {
+ candidate
+ } else {
+ install_dir.join(candidate)
+ };
+ if !source_path.is_file() {
+ return Err(format!(
+ "Credential file not found: {}",
+ path_to_string(&source_path)
+ ));
+ }
+ let target_path = PathBuf::from(target.trim());
+ if target_path.as_os_str().is_empty() {
+ return Err("Credential export target is empty".to_string());
+ }
+ fs::copy(&source_path, &target_path).map_err(|error| {
+ format!(
+ "Failed to copy {} to {}: {error}",
+ path_to_string(&source_path),
+ path_to_string(&target_path)
+ )
+ })?;
+ Ok(())
+}
+
#[tauri::command]
pub(crate) fn open_auth_files_directory(
gui_config_state: tauri::State<'_, GuiConfigState>,
@@ -352,6 +382,10 @@ pub(crate) fn management_endpoint(config: &GuiConfigFile, path: &str) -> Result<
config.port,
current_core_tls_settings()?.enabled,
);
+ // Plugin panel routes and the plugin quota API are only served on the v0 mux.
+ if let Some(rest) = path.strip_prefix("v0/") {
+ return Ok(format!("{origin}/v0/{rest}"));
+ }
Ok(format!("{origin}/v8/management/{path}"))
}
diff --git a/src/assets/icons/mimo.svg b/src/assets/icons/mimo.svg
new file mode 100644
index 000000000..71d0dc023
--- /dev/null
+++ b/src/assets/icons/mimo.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/plugin.svg b/src/assets/icons/plugin.svg
new file mode 100644
index 000000000..8f1402ff3
--- /dev/null
+++ b/src/assets/icons/plugin.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/qoder.svg b/src/assets/icons/qoder.svg
new file mode 100644
index 000000000..1e9716952
--- /dev/null
+++ b/src/assets/icons/qoder.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/trae.svg b/src/assets/icons/trae.svg
new file mode 100644
index 000000000..a64bd4846
--- /dev/null
+++ b/src/assets/icons/trae.svg
@@ -0,0 +1 @@
+
diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts
index 305223775..85fc2ad58 100644
--- a/src/i18n/ja.ts
+++ b/src/i18n/ja.ts
@@ -1699,4 +1699,15 @@ export const jaOverrides = {
'quota.service.error.unrecognized': '上流からクォータデータが返されましたが、形式を認識できません',
'quota.service.error.codexResetOnly': 'リセットクォータを使用できるのは Codex 認証情報のみです',
'quota.service.error.missingConsumeAuthIndex': 'auth-index がないためリセットクォータを使用できません',
+ 'authFiles.export': 'エクスポート',
+ 'authFiles.exported': '{name} をエクスポートしました',
+ 'authFiles.exportFailed': 'エクスポートに失敗: {error}',
+ 'quota.plugin.pool': '全アカウント合計',
+ 'quota.plugin.poolDetail': '{count} アカウント · 残り {remain} / {size}',
+ 'quota.plugin.packages': 'パッケージ枠',
+ 'quota.plugin.packagesDetail': '残り {remain} / {size} · {count} パッケージ',
+ 'quota.plugin.regionDetail': '残り {remain} / {size}',
+ 'quota.plugin.packCount': '{count} パッケージ',
+ 'quota.plugin.checkin': 'チェックイン積分',
+ 'quota.plugin.checkinDetail': '本日 +{today} · 累計 {total} · {streak} 日連続',
} satisfies Record;
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 62dc5101d..b48e70348 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -1699,4 +1699,15 @@ export const en: Record = {
'quota.service.error.unrecognized': 'Upstream returned quota data in an unrecognized format',
'quota.service.error.codexResetOnly': 'Only Codex credentials support reset credits',
'quota.service.error.missingConsumeAuthIndex': 'Missing auth-index; the reset credit cannot be used',
+ 'authFiles.export': 'Export',
+ 'authFiles.exported': 'Exported {name}',
+ 'authFiles.exportFailed': 'Export failed: {error}',
+ 'quota.plugin.pool': 'All accounts',
+ 'quota.plugin.poolDetail': '{count} accounts · {remain} of {size} left',
+ 'quota.plugin.packages': 'Package quota',
+ 'quota.plugin.packagesDetail': '{remain} of {size} left · {count} packages',
+ 'quota.plugin.regionDetail': '{remain} of {size} left',
+ 'quota.plugin.packCount': '{count} packages',
+ 'quota.plugin.checkin': 'Check-in credits',
+ 'quota.plugin.checkinDetail': '+{today} today · {total} total · {streak} day streak',
};
diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts
index c58df9f8f..912a9c8e4 100644
--- a/src/i18n/locales/zh-CN.ts
+++ b/src/i18n/locales/zh-CN.ts
@@ -1698,6 +1698,17 @@ export const zhCN = {
'quota.service.error.unrecognized': '上游返回了配额数据,但无法识别',
'quota.service.error.codexResetOnly': '只有 Codex 凭据支持使用重置额度',
'quota.service.error.missingConsumeAuthIndex': '缺少 auth-index,无法使用重置额度',
+ 'authFiles.export': '导出',
+ 'authFiles.exported': '已导出 {name}',
+ 'authFiles.exportFailed': '导出失败:{error}',
+ 'quota.plugin.pool': '账号池合计',
+ 'quota.plugin.poolDetail': '{count} 个账号 · 剩余 {remain} / 共 {size}',
+ 'quota.plugin.packages': '套餐额度',
+ 'quota.plugin.packagesDetail': '剩余 {remain} / 共 {size} · {count} 个套餐包',
+ 'quota.plugin.regionDetail': '剩余 {remain} / 共 {size}',
+ 'quota.plugin.packCount': '{count} 个套餐包',
+ 'quota.plugin.checkin': '签到积分',
+ 'quota.plugin.checkinDetail': '今日 +{today} · 累计 {total} · 连续 {streak} 天',
} as const;
export type MessageKey = keyof typeof zhCN;
diff --git a/src/pages/AuthFileManagementPage.tsx b/src/pages/AuthFileManagementPage.tsx
index 50c4d0899..08333a1eb 100644
--- a/src/pages/AuthFileManagementPage.tsx
+++ b/src/pages/AuthFileManagementPage.tsx
@@ -1,4 +1,5 @@
import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { save } from '@tauri-apps/plugin-dialog';
import { useConfirmation } from '../components/ConfirmationDialog';
import { QuotaActionFeedback } from '../components/QuotaActionFeedback';
import { AuthFileQuotaPanel } from '../components/AuthFileQuotaPanel';
@@ -30,9 +31,15 @@ import geminiIcon from '../assets/icons/gemini.svg';
import grokIcon from '../assets/icons/grok.svg';
import devinIcon from '../assets/icons/devin.svg';
import kimiIcon from '../assets/icons/kimi-light.svg';
+import workbuddyIcon from '../assets/icons/workbuddy.png';
+import traeIcon from '../assets/icons/trae.svg';
+import qoderIcon from '../assets/icons/qoder.svg';
+import zcodeIcon from '../assets/icons/zcode.png';
+import mimoIcon from '../assets/icons/mimo.svg';
import vertexIcon from '../assets/icons/vertex.svg';
import {
formatDate,
+ isRecord,
managementApi,
readBoolean,
readNumber,
@@ -88,6 +95,86 @@ const providerIcons: Record = {
vertex: vertexIcon,
xai: grokIcon,
devin: devinIcon,
+ workbuddy: workbuddyIcon,
+ trae: traeIcon,
+ qoder: qoderIcon,
+ zcode: zcodeIcon,
+ mimo: mimoIcon,
+};
+
+// Third-party exports (for example cockpit-style `workbuddy_accounts_*.json`) ship a
+// JSON array of flat snake_case accounts, while CPA stores one nested camelCase file
+// per credential. Split the array and reshape every entry before uploading it.
+const REGION_BY_DOMAIN: Record = {
+ 'copilot.tencent.com': 'cn',
+ 'codebuddy.cn': 'cn',
+ 'www.codebuddy.cn': 'cn',
+ 'workbuddy.ai': 'global',
+ 'www.workbuddy.ai': 'global',
+ 'codebuddy.ai': 'intl',
+ 'www.codebuddy.ai': 'intl',
+};
+
+const normalizeImportedCredential = (
+ value: Record,
+ fallbackProvider: string,
+): Record => {
+ if (isRecord(value.auth) || isRecord(value.account)) return value;
+ const accessToken = readString(value, 'accessToken', 'access_token');
+ if (!accessToken) return value;
+ const domain = readString(value, 'domain') || 'www.codebuddy.cn';
+ const provider = readString(value, 'provider', 'type', 'account_type') || fallbackProvider;
+ const rawExpiry = value.expiresAt ?? value.expires_at;
+ let expiresAt = 0;
+ if (typeof rawExpiry === 'number' && Number.isFinite(rawExpiry)) {
+ expiresAt = rawExpiry > 10 ** 11 ? Math.floor(rawExpiry / 1000) : Math.floor(rawExpiry);
+ }
+ return {
+ account: {
+ enterpriseId: '',
+ nickname: readString(value, 'nickname', 'name'),
+ uid: readString(value, 'uid', 'user_id'),
+ },
+ auth: {
+ accessToken,
+ refreshToken: readString(value, 'refreshToken', 'refresh_token'),
+ expiresAt,
+ domain,
+ region: readString(value, 'region') || REGION_BY_DOMAIN[domain] || 'cn',
+ },
+ auth_kind: 'oauth',
+ disabled: false,
+ provider,
+ type: provider,
+ };
+};
+
+const importCredentialFile = async (file: File): Promise => {
+ let parsed: unknown = null;
+ try {
+ parsed = JSON.parse(await file.text());
+ } catch {
+ parsed = null;
+ }
+ const entries = (Array.isArray(parsed) ? parsed : [parsed]).filter(isRecord);
+ const alreadyCredential =
+ entries.length === 1 && (isRecord(entries[0].auth) || isRecord(entries[0].account));
+ if (parsed === null || entries.length === 0 || alreadyCredential) {
+ await managementApi.uploadAuthFile(file);
+ return 1;
+ }
+ const fallbackProvider = file.name.split(/[_.-]/)[0].toLowerCase();
+ let uploaded = 0;
+ for (const entry of entries) {
+ const normalized = normalizeImportedCredential(entry, fallbackProvider);
+ const provider =
+ readString(normalized, 'provider', 'type', 'account_type') || fallbackProvider || 'credential';
+ const uid = isRecord(normalized.account) ? readString(normalized.account, 'uid') : '';
+ const name = uid ? `${provider}-${uid}.json` : file.name;
+ await managementApi.uploadAuthFileContent(name, JSON.stringify(normalized));
+ uploaded += 1;
+ }
+ return uploaded;
};
const providerName = (file: AuthFile) => {
@@ -294,8 +381,7 @@ export function AuthFileManagementPage() {
const failures: string[] = [];
for (const file of selected) {
try {
- await managementApi.uploadAuthFile(file);
- uploaded += 1;
+ uploaded += await importCredentialFile(file);
} catch (requestError) {
failures.push(`${file.name}:${String(requestError)}`);
}
@@ -311,6 +397,31 @@ export function AuthFileManagementPage() {
}
};
+ const exportFile = async (file: AuthFile) => {
+ feedback.clearNotice();
+ setError('');
+ const name = fileName(file);
+ const source = readString(file, 'path');
+ if (!source) {
+ setError(t('authFiles.exportFailed', { error: t('authFiles.fileOnly') }));
+ return;
+ }
+ setBusy(true);
+ try {
+ const target = await save({
+ defaultPath: name,
+ filters: [{ name: 'JSON', extensions: ['json'] }],
+ });
+ if (!target) return;
+ await managementApi.exportAuthFile(source, target);
+ showNotice({ key: 'authFiles.exported', variables: { name } });
+ } catch (requestError) {
+ setError(t('authFiles.exportFailed', { error: String(requestError) }));
+ } finally {
+ setBusy(false);
+ }
+ };
+
const toggleStatus = async (file: AuthFile) => {
feedback.clearNotice();
setBusy(true);
@@ -453,6 +564,7 @@ export function AuthFileManagementPage() {
{providerKey(file) ? : null}
+
diff --git a/src/pages/ManagementPages.tsx b/src/pages/ManagementPages.tsx
index fefaefbcb..98e5067bf 100644
--- a/src/pages/ManagementPages.tsx
+++ b/src/pages/ManagementPages.tsx
@@ -14,6 +14,11 @@ import codexIcon from '../assets/icons/codex.svg';
import grokIcon from '../assets/icons/grok.svg';
import devinIcon from '../assets/icons/devin.svg';
import kimiIcon from '../assets/icons/kimi-light.svg';
+import workbuddyIcon from '../assets/icons/workbuddy.png';
+import traeIcon from '../assets/icons/trae.svg';
+import qoderIcon from '../assets/icons/qoder.svg';
+import zcodeIcon from '../assets/icons/zcode.png';
+import mimoIcon from '../assets/icons/mimo.svg';
import { useI18n } from '../i18n';
import { FloatingNotice, useAppNotice } from '../appNotice';
import { oauthSubpages, type OAuthSubpage } from '../oauthNavigation';
@@ -32,7 +37,18 @@ import { QuotaPage } from './QuotaPage';
import { validateDevinCallback } from '../services/devinOAuth';
import { handleHorizontalTabKey } from '../components/tabKeyboardNavigation';
-type OAuthProviderId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'xai' | 'devin';
+type OAuthProviderId =
+ | 'codex'
+ | 'claude'
+ | 'antigravity'
+ | 'kimi'
+ | 'xai'
+ | 'devin'
+ | 'workbuddy'
+ | 'trae'
+ | 'qoder'
+ | 'zcode'
+ | 'mimo';
type OAuthFlowStatus = 'idle' | 'waiting' | 'success' | 'error';
type OAuthProviderState = {
@@ -72,6 +88,11 @@ const oauthProviders = [
{ id: 'kimi' as const, name: 'Kimi OAuth', icon: kimiIcon },
{ id: 'xai' as const, name: 'xAI OAuth', icon: grokIcon },
{ id: 'devin' as const, name: 'Devin OAuth', icon: devinIcon },
+ { id: 'workbuddy' as const, name: 'WorkBuddy OAuth', icon: workbuddyIcon },
+ { id: 'trae' as const, name: 'Trae OAuth', icon: traeIcon },
+ { id: 'qoder' as const, name: 'Qoder OAuth', icon: qoderIcon },
+ { id: 'zcode' as const, name: 'ZCode OAuth', icon: zcodeIcon },
+ { id: 'mimo' as const, name: 'MiMo OAuth', icon: mimoIcon },
];
const OAUTH_CALLBACK_SUPPORTED = new Set([
diff --git a/src/pages/QuotaPage.tsx b/src/pages/QuotaPage.tsx
index 861e343ac..d8366be9b 100644
--- a/src/pages/QuotaPage.tsx
+++ b/src/pages/QuotaPage.tsx
@@ -11,6 +11,7 @@ import codexIcon from '../assets/icons/codex.svg';
import grokIcon from '../assets/icons/grok.svg';
import devinIcon from '../assets/icons/devin.svg';
import kimiIcon from '../assets/icons/kimi-light.svg';
+import pluginIcon from '../assets/icons/plugin.svg';
import { managementApi, readBoolean, responseList } from '../services/managementApi';
import { formatQuotaReset, useQuotaClock } from '../services/quotaTime';
import {
@@ -42,9 +43,10 @@ const providerMeta: Record = {
xai: { label: 'xAI', icon: grokIcon },
devin: { label: 'Devin', icon: devinIcon },
antigravity: { label: 'Antigravity', icon: antigravityIcon },
+ plugin: { label: 'Plugins', icon: pluginIcon },
};
-const providerOrder: QuotaProvider[] = ['claude', 'antigravity', 'codex', 'xai', 'kimi', 'devin'];
+const providerOrder: QuotaProvider[] = ['claude', 'antigravity', 'codex', 'xai', 'kimi', 'devin', 'plugin'];
const REFRESH_CONCURRENCY = 4;
export function QuotaPage() {
diff --git a/src/services/managementApi.ts b/src/services/managementApi.ts
index fb7dc5798..c5fde0b1a 100644
--- a/src/services/managementApi.ts
+++ b/src/services/managementApi.ts
@@ -253,6 +253,12 @@ export const managementApi = {
data,
});
},
+ uploadAuthFileContent: async (name: string, content: string) => {
+ const data = Array.from(new TextEncoder().encode(content));
+ return invoke('upload_auth_file', { name, data });
+ },
+ exportAuthFile: (source: string, target: string) =>
+ invoke('export_auth_file', { source, target }),
openAuthFilesDirectory: () => invoke('open_auth_files_directory'),
};
diff --git a/src/services/quotaService.ts b/src/services/quotaService.ts
index c9b1745a4..86049b330 100644
--- a/src/services/quotaService.ts
+++ b/src/services/quotaService.ts
@@ -18,7 +18,14 @@ const quotaText = (
) => translate(getCurrentLocale(), key, variables);
export type AuthFile = Record;
-export type QuotaProvider = 'claude' | 'codex' | 'kimi' | 'xai' | 'antigravity' | 'devin';
+export type QuotaProvider =
+ | 'claude'
+ | 'codex'
+ | 'kimi'
+ | 'xai'
+ | 'antigravity'
+ | 'devin'
+ | 'plugin';
export type QuotaStatus = 'idle' | 'loading' | 'success' | 'error';
export type QuotaRow = {
label: string;
@@ -52,6 +59,8 @@ const endpointByProvider: Record = {
kimi: 'https://api.kimi.com/coding/v1/usages',
xai: 'https://cli-chat-proxy.grok.com/v1/billing',
antigravity: 'https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary',
+ // Plugin providers are answered by the plugin management API; there is no upstream endpoint.
+ plugin: '',
};
const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
@@ -88,10 +97,16 @@ const headersByProvider: Record> = {
'Content-Type': 'application/json',
'User-Agent': 'antigravity/cli/1.0.13 (aidev_client; os_type=darwin; arch=arm64)',
},
+ // The plugin API injects the credential server-side; the client only sends the auth index.
+ plugin: {},
};
+// cpa-multi-plugins providers that expose a quota summary through their plugin API.
+const PLUGIN_QUOTA_PROVIDERS = ['workbuddy', 'trae', 'qoder', 'zcode', 'mimo'];
+
export const providerForFile = (file: AuthFile): QuotaProvider | null => {
const value = readString(file, 'provider', 'type', 'account_type').toLowerCase().replace(/_/g, '-');
+ if (PLUGIN_QUOTA_PROVIDERS.includes(value)) return 'plugin';
if (value === 'x-ai' || value === 'grok') return 'xai';
if (value === 'cognition') return 'devin';
if (value === 'anthropic') return 'claude';
@@ -836,6 +851,86 @@ const callCodexResetCredits = async (
return codexResetCreditDetailsFor(payload);
};
+const pluginNumber = (value: unknown): number | null =>
+ (typeof value === 'number' && Number.isFinite(value) ? value : null);
+
+// Plugin quota is resolved by the plugin's own management API. The bundled
+// cpa-multi-plugins providers share one payload shape: `summary` carries the
+// per-region and total pack numbers, `accounts[].checkin` the sign-in credits.
+async function loadPluginQuota(file: AuthFile): Promise {
+ const pluginId = readString(file, 'provider', 'type', 'account_type').toLowerCase().replace(/_/g, '-');
+ if (!/^[a-z0-9-]+$/.test(pluginId)) throw new Error(quotaText('quota.service.error.unrecognized'));
+ const payload = await managementApi.post>(
+ `/v0/management/plugins/${pluginId}/refresh`,
+ undefined,
+ { timeoutMs: 30_000 },
+ );
+ if (!isRecord(payload)) throw new Error(quotaText('quota.service.error.noResponse'));
+ const summary = isRecord(payload.summary) ? payload.summary : {};
+ const accounts = Array.isArray(payload.accounts) ? payload.accounts.filter(isRecord) : [];
+ const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
+ const credentialName = readString(file, 'name', 'auth_id');
+ const own = accounts.find((account) => {
+ const index = normalizeAuthIndex(account.auth_index ?? account.authIndex);
+ if (authIndex && index) return index === authIndex;
+ const id = readString(account, 'auth_id', 'name');
+ return Boolean(credentialName && id && id === credentialName);
+ }) ?? (accounts.length === 1 ? accounts[0] : null);
+ const credits = isRecord(own?.credits) ? (own?.credits as Record) : null;
+ const totalSize = pluginNumber(credits?.total_size) ?? 0;
+ const totalRemain = pluginNumber(credits?.total_remain) ?? 0;
+ const packCount = pluginNumber(credits?.pack_count) ?? 0;
+ const rows: QuotaRow[] = [];
+ if (totalSize > 0) {
+ rows.push({
+ label: quotaText('quota.plugin.packages'),
+ remainingPercent: Math.max(0, Math.min(100, (totalRemain / totalSize) * 100)),
+ detail: quotaText('quota.plugin.packagesDetail', {
+ remain: totalRemain, size: totalSize, count: packCount,
+ }),
+ });
+ }
+ const checkin = isRecord(own?.checkin) ? (own?.checkin as Record) : null;
+ if (checkin) {
+ const totalCredits = pluginNumber(checkin.total_credits) ?? 0;
+ if (totalCredits > 0) {
+ rows.push({
+ label: quotaText('quota.plugin.checkin'),
+ remainingPercent: null,
+ detail: quotaText('quota.plugin.checkinDetail', {
+ today: pluginNumber(checkin.today_credit) ?? 0,
+ total: totalCredits,
+ streak: pluginNumber(checkin.streak_days) ?? 0,
+ }),
+ });
+ }
+ }
+ // The plugin answers with every account it owns; keep the pooled totals as a
+ // trailing row so a multi-account pool stays visible from any credential.
+ const poolSize = pluginNumber(summary.total_size) ?? 0;
+ if (poolSize > 0 && accounts.length > 1) {
+ const poolRemain = pluginNumber(summary.total_remain) ?? 0;
+ rows.push({
+ label: quotaText('quota.plugin.pool'),
+ remainingPercent: Math.max(0, Math.min(100, (poolRemain / poolSize) * 100)),
+ detail: quotaText('quota.plugin.poolDetail', {
+ count: accounts.length, remain: poolRemain, size: poolSize,
+ }),
+ });
+ }
+ if (rows.length === 0) throw new Error(quotaText('quota.service.error.unrecognized'));
+ return {
+ status: 'success',
+ rows,
+ plan: own && readString(own, 'nickname')
+ ? readString(own, 'nickname')
+ : packCount > 0
+ ? quotaText('quota.plugin.packCount', { count: packCount })
+ : undefined,
+ fetchedAt: Date.now(),
+ };
+}
+
async function loadQuotaSnapshot(file: AuthFile): Promise {
const provider = providerForFile(file);
if (!provider) {
@@ -847,6 +942,7 @@ async function loadQuotaSnapshot(file: AuthFile): Promise {
}
try {
if (booleanValue(file.disabled) === true) throw new Error(quotaText('quota.fileDisabled'));
+ if (provider === 'plugin') return await loadPluginQuota(file);
const codexMetadata = provider === 'codex' ? codexMetadataFor(file) : undefined;
const codexAccountId = codexMetadata?.accountId || '';
const responseClock: { serverTimeOffsetMs?: number } = {};