mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat: add xAI/Grok quota management
- Introduced new quota management for xAI/Grok, including types, API calls, and UI components. - Added localization support for xAI quota in English, Russian, Chinese (Simplified and Traditional). - Updated styles for xAI quota cards and sections. - Integrated xAI quota fetching and rendering logic into existing quota management system. - Enhanced auth file handling to support xAI files.
This commit is contained in:
@@ -5,5 +5,12 @@
|
||||
export { QuotaSection } from './QuotaSection';
|
||||
export { QuotaCard } from './QuotaCard';
|
||||
export { useQuotaLoader } from './useQuotaLoader';
|
||||
export { ANTIGRAVITY_CONFIG, CLAUDE_CONFIG, CODEX_CONFIG, GEMINI_CLI_CONFIG, KIMI_CONFIG } from './quotaConfigs';
|
||||
export {
|
||||
ANTIGRAVITY_CONFIG,
|
||||
CLAUDE_CONFIG,
|
||||
CODEX_CONFIG,
|
||||
GEMINI_CLI_CONFIG,
|
||||
KIMI_CONFIG,
|
||||
XAI_CONFIG,
|
||||
} from './quotaConfigs';
|
||||
export type { QuotaConfig } from './quotaConfigs';
|
||||
|
||||
@@ -28,6 +28,9 @@ import type {
|
||||
GeminiCliUserTier,
|
||||
KimiQuotaRow,
|
||||
KimiQuotaState,
|
||||
XaiBillingConfig,
|
||||
XaiBillingSummary,
|
||||
XaiQuotaState,
|
||||
} from '@/types';
|
||||
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
|
||||
import { useQuotaStore } from '@/stores';
|
||||
@@ -45,6 +48,8 @@ import {
|
||||
GEMINI_CLI_REQUEST_HEADERS,
|
||||
KIMI_USAGE_URL,
|
||||
KIMI_REQUEST_HEADERS,
|
||||
XAI_BILLING_URL,
|
||||
XAI_REQUEST_HEADERS,
|
||||
normalizeGeminiCliModelId,
|
||||
normalizeNumberValue,
|
||||
normalizePlanType,
|
||||
@@ -56,6 +61,7 @@ import {
|
||||
parseGeminiCliQuotaPayload,
|
||||
parseGeminiCliCodeAssistPayload,
|
||||
parseKimiUsagePayload,
|
||||
parseXaiBillingPayload,
|
||||
resolveCodexChatgptAccountId,
|
||||
resolveCodexPlanType,
|
||||
resolveGeminiCliProjectId,
|
||||
@@ -74,6 +80,7 @@ import {
|
||||
isGeminiCliFile,
|
||||
isKimiFile,
|
||||
isRuntimeOnlyAuthFile,
|
||||
isXaiFile,
|
||||
} from '@/utils/quota';
|
||||
import { normalizeAuthIndex } from '@/utils/authIndex';
|
||||
import type { QuotaRenderHelpers } from './QuotaCard';
|
||||
@@ -81,7 +88,7 @@ import styles from '@/pages/QuotaPage.module.scss';
|
||||
|
||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||
|
||||
type QuotaType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi';
|
||||
type QuotaType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi' | 'xai';
|
||||
|
||||
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
||||
const QUOTA_PROGRESS_HIGH_THRESHOLD = 70;
|
||||
@@ -89,7 +96,12 @@ const QUOTA_PROGRESS_MEDIUM_THRESHOLD = 30;
|
||||
const geminiCliSupplementaryRequestIds = new Map<string, number>();
|
||||
const geminiCliSupplementaryCache = new Map<
|
||||
string,
|
||||
{ requestId: number; tierLabel: string | null; tierId: string | null; creditBalance: number | null }
|
||||
{
|
||||
requestId: number;
|
||||
tierLabel: string | null;
|
||||
tierId: string | null;
|
||||
creditBalance: number | null;
|
||||
}
|
||||
>();
|
||||
|
||||
export interface QuotaStore {
|
||||
@@ -98,11 +110,13 @@ export interface QuotaStore {
|
||||
codexQuota: Record<string, CodexQuotaState>;
|
||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||
kimiQuota: Record<string, KimiQuotaState>;
|
||||
xaiQuota: Record<string, XaiQuotaState>;
|
||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||
setClaudeQuota: (updater: QuotaUpdater<Record<string, ClaudeQuotaState>>) => void;
|
||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||
setKimiQuota: (updater: QuotaUpdater<Record<string, KimiQuotaState>>) => void;
|
||||
setXaiQuota: (updater: QuotaUpdater<Record<string, XaiQuotaState>>) => void;
|
||||
clearQuotaCache: () => void;
|
||||
}
|
||||
|
||||
@@ -233,12 +247,19 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
|
||||
const WINDOW_META = {
|
||||
codeFiveHour: { id: 'five-hour', labelKey: 'codex_quota.primary_window' },
|
||||
codeWeekly: { id: 'weekly', labelKey: 'codex_quota.secondary_window' },
|
||||
codeReviewFiveHour: { id: 'code-review-five-hour', labelKey: 'codex_quota.code_review_primary_window' },
|
||||
codeReviewWeekly: { id: 'code-review-weekly', labelKey: 'codex_quota.code_review_secondary_window' },
|
||||
codeReviewFiveHour: {
|
||||
id: 'code-review-five-hour',
|
||||
labelKey: 'codex_quota.code_review_primary_window',
|
||||
},
|
||||
codeReviewWeekly: {
|
||||
id: 'code-review-weekly',
|
||||
labelKey: 'codex_quota.code_review_secondary_window',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const rateLimit = payload.rate_limit ?? payload.rateLimit ?? undefined;
|
||||
const codeReviewLimit = payload.code_review_rate_limit ?? payload.codeReviewRateLimit ?? undefined;
|
||||
const codeReviewLimit =
|
||||
payload.code_review_rate_limit ?? payload.codeReviewRateLimit ?? undefined;
|
||||
const additionalRateLimits = payload.additional_rate_limits ?? payload.additionalRateLimits ?? [];
|
||||
const windows: CodexQuotaWindow[] = [];
|
||||
|
||||
@@ -302,7 +323,8 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
|
||||
fiveHourWindow = primaryWindow && primaryWindow !== weeklyWindow ? primaryWindow : null;
|
||||
}
|
||||
if (!weeklyWindow) {
|
||||
weeklyWindow = secondaryWindow && secondaryWindow !== fiveHourWindow ? secondaryWindow : null;
|
||||
weeklyWindow =
|
||||
secondaryWindow && secondaryWindow !== fiveHourWindow ? secondaryWindow : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +392,8 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
|
||||
|
||||
const idPrefix = normalizeWindowId(limitName) || `additional-${index + 1}`;
|
||||
const additionalPrimaryWindow = rateInfo.primary_window ?? rateInfo.primaryWindow ?? null;
|
||||
const additionalSecondaryWindow = rateInfo.secondary_window ?? rateInfo.secondaryWindow ?? null;
|
||||
const additionalSecondaryWindow =
|
||||
rateInfo.secondary_window ?? rateInfo.secondaryWindow ?? null;
|
||||
const additionalLimitReached = rateInfo.limit_reached ?? rateInfo.limitReached;
|
||||
const additionalAllowed = rateInfo.allowed;
|
||||
|
||||
@@ -456,8 +479,7 @@ const resolveGeminiCliTierLabel = (
|
||||
if (!payload) return null;
|
||||
const currentTier: GeminiCliUserTier | null | undefined =
|
||||
payload.currentTier ?? payload.current_tier;
|
||||
const paidTier: GeminiCliUserTier | null | undefined =
|
||||
payload.paidTier ?? payload.paid_tier;
|
||||
const paidTier: GeminiCliUserTier | null | undefined = payload.paidTier ?? payload.paid_tier;
|
||||
const rawId = normalizeStringValue(paidTier?.id) ?? normalizeStringValue(currentTier?.id);
|
||||
if (!rawId) return null;
|
||||
const tierId = rawId.toLowerCase();
|
||||
@@ -465,14 +487,11 @@ const resolveGeminiCliTierLabel = (
|
||||
return labelKey ? t(`gemini_cli_quota.${labelKey}`) : rawId;
|
||||
};
|
||||
|
||||
const resolveGeminiCliTierId = (
|
||||
payload: GeminiCliCodeAssistPayload | null
|
||||
): string | null => {
|
||||
const resolveGeminiCliTierId = (payload: GeminiCliCodeAssistPayload | null): string | null => {
|
||||
if (!payload) return null;
|
||||
const currentTier: GeminiCliUserTier | null | undefined =
|
||||
payload.currentTier ?? payload.current_tier;
|
||||
const paidTier: GeminiCliUserTier | null | undefined =
|
||||
payload.paidTier ?? payload.paid_tier;
|
||||
const paidTier: GeminiCliUserTier | null | undefined = payload.paidTier ?? payload.paid_tier;
|
||||
const rawId = normalizeStringValue(paidTier?.id) ?? normalizeStringValue(currentTier?.id);
|
||||
return rawId ? rawId.toLowerCase() : null;
|
||||
};
|
||||
@@ -481,14 +500,12 @@ const resolveGeminiCliCreditBalance = (
|
||||
payload: GeminiCliCodeAssistPayload | null
|
||||
): number | null => {
|
||||
if (!payload) return null;
|
||||
const paidTier: GeminiCliUserTier | null | undefined =
|
||||
payload.paidTier ?? payload.paid_tier;
|
||||
const paidTier: GeminiCliUserTier | null | undefined = payload.paidTier ?? payload.paid_tier;
|
||||
const currentTier: GeminiCliUserTier | null | undefined =
|
||||
payload.currentTier ?? payload.current_tier;
|
||||
const tier = paidTier ?? currentTier;
|
||||
if (!tier) return null;
|
||||
const credits: GeminiCliCredits[] =
|
||||
tier.availableCredits ?? tier.available_credits ?? [];
|
||||
const credits: GeminiCliCredits[] = tier.availableCredits ?? tier.available_credits ?? [];
|
||||
let total = 0;
|
||||
let found = false;
|
||||
for (const credit of credits) {
|
||||
@@ -859,7 +876,11 @@ const renderGeminiCliItems = (
|
||||
|
||||
if (buckets.length === 0) {
|
||||
nodes.push(
|
||||
h('div', { key: 'empty', className: styleMap.quotaMessage }, t('gemini_cli_quota.empty_buckets'))
|
||||
h(
|
||||
'div',
|
||||
{ key: 'empty', className: styleMap.quotaMessage },
|
||||
t('gemini_cli_quota.empty_buckets')
|
||||
)
|
||||
);
|
||||
return h(Fragment, null, ...nodes);
|
||||
}
|
||||
@@ -973,8 +994,12 @@ const resolveClaudePlanType = (profile: ClaudeProfileResponse | null): string |
|
||||
const hasClaudePro = normalizeFlagValue(profile.account?.has_claude_pro);
|
||||
if (hasClaudePro) return 'plan_pro';
|
||||
|
||||
const organizationType = normalizeStringValue(profile.organization?.organization_type)?.toLowerCase();
|
||||
const subscriptionStatus = normalizeStringValue(profile.organization?.subscription_status)?.toLowerCase();
|
||||
const organizationType = normalizeStringValue(
|
||||
profile.organization?.organization_type
|
||||
)?.toLowerCase();
|
||||
const subscriptionStatus = normalizeStringValue(
|
||||
profile.organization?.subscription_status
|
||||
)?.toLowerCase();
|
||||
|
||||
if (organizationType === 'claude_team' && subscriptionStatus === 'active') {
|
||||
return 'plan_team';
|
||||
@@ -988,7 +1013,11 @@ const resolveClaudePlanType = (profile: ClaudeProfileResponse | null): string |
|
||||
const fetchClaudeQuota = async (
|
||||
file: AuthFileItem,
|
||||
t: TFunction
|
||||
): Promise<{ windows: ClaudeQuotaWindow[]; extraUsage?: ClaudeExtraUsage | null; planType?: string | null }> => {
|
||||
): Promise<{
|
||||
windows: ClaudeQuotaWindow[];
|
||||
extraUsage?: ClaudeExtraUsage | null;
|
||||
planType?: string | null;
|
||||
}> => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndex = normalizeAuthIndex(rawAuthIndex);
|
||||
if (!authIndex) {
|
||||
@@ -1217,7 +1246,13 @@ export const GEMINI_CLI_CONFIG: QuotaConfig<
|
||||
fetchQuota: fetchGeminiCliQuota,
|
||||
storeSelector: (state) => state.geminiCliQuota,
|
||||
storeSetter: 'setGeminiCliQuota',
|
||||
buildLoadingState: () => ({ status: 'loading', buckets: [], tierLabel: null, tierId: null, creditBalance: null }),
|
||||
buildLoadingState: () => ({
|
||||
status: 'loading',
|
||||
buckets: [],
|
||||
tierLabel: null,
|
||||
tierId: null,
|
||||
creditBalance: null,
|
||||
}),
|
||||
buildSuccessState: (data) => {
|
||||
const supplementarySnapshot = readGeminiCliSupplementarySnapshot(
|
||||
data.fileName,
|
||||
@@ -1245,10 +1280,7 @@ export const GEMINI_CLI_CONFIG: QuotaConfig<
|
||||
renderQuotaItems: renderGeminiCliItems,
|
||||
};
|
||||
|
||||
const fetchKimiQuota = async (
|
||||
file: AuthFileItem,
|
||||
t: TFunction
|
||||
): Promise<KimiQuotaRow[]> => {
|
||||
const fetchKimiQuota = async (file: AuthFileItem, t: TFunction): Promise<KimiQuotaRow[]> => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndex = normalizeAuthIndex(rawAuthIndex);
|
||||
if (!authIndex) {
|
||||
@@ -1299,7 +1331,7 @@ const renderKimiItems = (
|
||||
const percentLabel = remaining === null ? '--' : `${remaining}%`;
|
||||
const rowLabel = row.labelKey
|
||||
? t(row.labelKey, (row.labelParams ?? {}) as Record<string, string | number>)
|
||||
: row.label ?? '';
|
||||
: (row.label ?? '');
|
||||
const resetLabel = formatKimiResetHint(t, row.resetHint);
|
||||
|
||||
return h(
|
||||
@@ -1313,12 +1345,8 @@ const renderKimiItems = (
|
||||
'div',
|
||||
{ className: styleMap.quotaMeta },
|
||||
h('span', { className: styleMap.quotaPercent }, percentLabel),
|
||||
limit > 0
|
||||
? h('span', { className: styleMap.quotaAmount }, `${used} / ${limit}`)
|
||||
: null,
|
||||
resetLabel
|
||||
? h('span', { className: styleMap.quotaReset }, resetLabel)
|
||||
: null
|
||||
limit > 0 ? h('span', { className: styleMap.quotaAmount }, `${used} / ${limit}`) : null,
|
||||
resetLabel ? h('span', { className: styleMap.quotaReset }, resetLabel) : null
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, {
|
||||
@@ -1330,6 +1358,151 @@ const renderKimiItems = (
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeXaiCentValue = (value: XaiBillingConfig['monthlyLimit']): number | null => {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return normalizeNumberValue((value as { val?: unknown }).val);
|
||||
}
|
||||
return normalizeNumberValue(value);
|
||||
};
|
||||
|
||||
const buildXaiBillingSummary = (
|
||||
config: XaiBillingConfig | null | undefined
|
||||
): XaiBillingSummary | null => {
|
||||
if (!config || typeof config !== 'object') return null;
|
||||
|
||||
const monthlyLimitCents = normalizeXaiCentValue(config.monthlyLimit ?? config.monthly_limit);
|
||||
const usedCents = normalizeXaiCentValue(config.used);
|
||||
const onDemandCapCents = normalizeXaiCentValue(config.onDemandCap ?? config.on_demand_cap);
|
||||
const billingPeriodStart =
|
||||
normalizeStringValue(config.billingPeriodStart ?? config.billing_period_start) ?? undefined;
|
||||
const billingPeriodEnd =
|
||||
normalizeStringValue(config.billingPeriodEnd ?? config.billing_period_end) ?? undefined;
|
||||
|
||||
if (
|
||||
monthlyLimitCents === null &&
|
||||
usedCents === null &&
|
||||
onDemandCapCents === null &&
|
||||
!billingPeriodEnd
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usedPercent =
|
||||
monthlyLimitCents !== null && monthlyLimitCents > 0 && usedCents !== null
|
||||
? (usedCents / monthlyLimitCents) * 100
|
||||
: null;
|
||||
|
||||
return {
|
||||
monthlyLimitCents,
|
||||
usedCents,
|
||||
onDemandCapCents,
|
||||
billingPeriodStart,
|
||||
billingPeriodEnd,
|
||||
usedPercent,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchXaiQuota = async (file: AuthFileItem, t: TFunction): Promise<XaiBillingSummary> => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndex = normalizeAuthIndex(rawAuthIndex);
|
||||
if (!authIndex) {
|
||||
throw new Error(t('xai_quota.missing_auth_index'));
|
||||
}
|
||||
|
||||
const result = await apiCallApi.request({
|
||||
authIndex,
|
||||
method: 'GET',
|
||||
url: XAI_BILLING_URL,
|
||||
header: { ...XAI_REQUEST_HEADERS },
|
||||
});
|
||||
|
||||
if (result.statusCode < 200 || result.statusCode >= 300) {
|
||||
throw createStatusError(getApiCallErrorMessage(result), result.statusCode);
|
||||
}
|
||||
|
||||
const payload = parseXaiBillingPayload(result.body ?? result.bodyText);
|
||||
const summary = buildXaiBillingSummary(payload?.config);
|
||||
if (!summary) {
|
||||
throw new Error(t('xai_quota.empty_data'));
|
||||
}
|
||||
|
||||
return summary;
|
||||
};
|
||||
|
||||
const formatUsdFromCents = (cents: number | null): string => {
|
||||
if (cents === null) return '--';
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(cents / 100);
|
||||
};
|
||||
|
||||
const formatXaiUsageAmount = (billing: XaiBillingSummary): string => {
|
||||
const used = formatUsdFromCents(billing.usedCents);
|
||||
const limit = formatUsdFromCents(billing.monthlyLimitCents);
|
||||
if (billing.monthlyLimitCents === null) return used;
|
||||
return `${used} / ${limit}`;
|
||||
};
|
||||
|
||||
const renderXaiItems = (
|
||||
quota: XaiQuotaState,
|
||||
t: TFunction,
|
||||
helpers: QuotaRenderHelpers
|
||||
): ReactNode => {
|
||||
const { styles: styleMap, QuotaProgressBar } = helpers;
|
||||
const { createElement: h, Fragment } = React;
|
||||
const billing = quota.billing;
|
||||
|
||||
if (!billing) {
|
||||
return h('div', { className: styleMap.quotaMessage }, t('xai_quota.empty_data'));
|
||||
}
|
||||
|
||||
const clampedUsed =
|
||||
billing.usedPercent === null ? null : Math.max(0, Math.min(100, billing.usedPercent));
|
||||
const remaining = clampedUsed === null ? null : Math.max(0, Math.min(100, 100 - clampedUsed));
|
||||
const percentLabel = remaining === null ? '--' : `${Math.round(remaining)}%`;
|
||||
const amountLabel = formatXaiUsageAmount(billing);
|
||||
const resetLabel = formatQuotaResetTime(billing.billingPeriodEnd);
|
||||
const onDemandCap = billing.onDemandCapCents ?? 0;
|
||||
const payAsYouGoLabel =
|
||||
onDemandCap > 0
|
||||
? t('xai_quota.pay_as_you_go_enabled', { cap: formatUsdFromCents(onDemandCap) })
|
||||
: t('xai_quota.pay_as_you_go_disabled');
|
||||
|
||||
return h(
|
||||
Fragment,
|
||||
null,
|
||||
h(
|
||||
'div',
|
||||
{ key: 'pay-as-you-go', className: styleMap.codexPlan },
|
||||
h('span', { className: styleMap.codexPlanLabel }, t('xai_quota.pay_as_you_go_label')),
|
||||
h('span', { className: styleMap.codexPlanValue }, payAsYouGoLabel)
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ key: 'monthly-credits', className: styleMap.quotaRow },
|
||||
h(
|
||||
'div',
|
||||
{ className: styleMap.quotaRowHeader },
|
||||
h('span', { className: styleMap.quotaModel }, t('xai_quota.monthly_credits')),
|
||||
h(
|
||||
'div',
|
||||
{ className: styleMap.quotaMeta },
|
||||
h('span', { className: styleMap.quotaPercent }, percentLabel),
|
||||
h('span', { className: styleMap.quotaAmount }, amountLabel),
|
||||
h('span', { className: styleMap.quotaReset }, resetLabel)
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, {
|
||||
percent: remaining,
|
||||
highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
|
||||
mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const KIMI_CONFIG: QuotaConfig<KimiQuotaState, KimiQuotaRow[]> = {
|
||||
type: 'kimi',
|
||||
i18nPrefix: 'kimi_quota',
|
||||
@@ -1352,3 +1525,26 @@ export const KIMI_CONFIG: QuotaConfig<KimiQuotaState, KimiQuotaRow[]> = {
|
||||
gridClassName: styles.kimiGrid,
|
||||
renderQuotaItems: renderKimiItems,
|
||||
};
|
||||
|
||||
export const XAI_CONFIG: QuotaConfig<XaiQuotaState, XaiBillingSummary> = {
|
||||
type: 'xai',
|
||||
i18nPrefix: 'xai_quota',
|
||||
cardIdleMessageKey: 'quota_management.card_idle_hint',
|
||||
filterFn: (file) => isXaiFile(file) && !isDisabledAuthFile(file),
|
||||
fetchQuota: fetchXaiQuota,
|
||||
storeSelector: (state) => state.xaiQuota,
|
||||
storeSetter: 'setXaiQuota',
|
||||
buildLoadingState: () => ({ status: 'loading', billing: null }),
|
||||
buildSuccessState: (billing) => ({ status: 'success', billing }),
|
||||
buildErrorState: (message, status) => ({
|
||||
status: 'error',
|
||||
billing: null,
|
||||
error: message,
|
||||
errorStatus: status,
|
||||
}),
|
||||
cardClassName: styles.xaiCard,
|
||||
controlsClassName: styles.xaiControls,
|
||||
controlClassName: styles.xaiControl,
|
||||
gridClassName: styles.xaiGrid,
|
||||
renderQuotaItems: renderXaiItems,
|
||||
};
|
||||
|
||||
@@ -112,7 +112,9 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
? styles.geminiCliCard
|
||||
: quotaType === 'kimi'
|
||||
? styles.kimiCard
|
||||
: '';
|
||||
: quotaType === 'xai'
|
||||
? styles.xaiCard
|
||||
: '';
|
||||
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(rawAuthIndex);
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
CLAUDE_CONFIG,
|
||||
CODEX_CONFIG,
|
||||
GEMINI_CLI_CONFIG,
|
||||
KIMI_CONFIG
|
||||
KIMI_CONFIG,
|
||||
XAI_CONFIG,
|
||||
} from '@/components/quota';
|
||||
import { useNotificationStore, useQuotaStore } from '@/stores';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
@@ -14,7 +15,7 @@ import { getStatusFromError } from '@/utils/quota';
|
||||
import {
|
||||
isRuntimeOnlyAuthFile,
|
||||
resolveQuotaErrorMessage,
|
||||
type QuotaProviderType
|
||||
type QuotaProviderType,
|
||||
} from '@/features/authFiles/constants';
|
||||
import { QuotaProgressBar } from '@/features/authFiles/components/QuotaProgressBar';
|
||||
import styles from '@/pages/AuthFilesPage.module.scss';
|
||||
@@ -26,6 +27,7 @@ const getQuotaConfig = (type: QuotaProviderType) => {
|
||||
if (type === 'claude') return CLAUDE_CONFIG;
|
||||
if (type === 'codex') return CODEX_CONFIG;
|
||||
if (type === 'kimi') return KIMI_CONFIG;
|
||||
if (type === 'xai') return XAI_CONFIG;
|
||||
return GEMINI_CLI_CONFIG;
|
||||
};
|
||||
|
||||
@@ -45,14 +47,18 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
if (quotaType === 'claude') return state.claudeQuota[file.name] as QuotaState;
|
||||
if (quotaType === 'codex') return state.codexQuota[file.name] as QuotaState;
|
||||
if (quotaType === 'kimi') return state.kimiQuota[file.name] as QuotaState;
|
||||
if (quotaType === 'xai') return state.xaiQuota[file.name] as QuotaState;
|
||||
return state.geminiCliQuota[file.name] as QuotaState;
|
||||
});
|
||||
|
||||
const updateQuotaState = useQuotaStore((state) => {
|
||||
if (quotaType === 'antigravity') return state.setAntigravityQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'claude') return state.setClaudeQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'antigravity')
|
||||
return state.setAntigravityQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'claude')
|
||||
return state.setClaudeQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'codex') return state.setCodexQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'kimi') return state.setKimiQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'xai') return state.setXaiQuota as unknown as (updater: unknown) => void;
|
||||
return state.setGeminiCliQuota as unknown as (updater: unknown) => void;
|
||||
});
|
||||
|
||||
@@ -73,14 +79,14 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
|
||||
updateQuotaState((prev: Record<string, unknown>) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildLoadingState()
|
||||
[file.name]: config.buildLoadingState(),
|
||||
}));
|
||||
|
||||
try {
|
||||
const data = await config.fetchQuota(file, t);
|
||||
updateQuotaState((prev: Record<string, unknown>) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildSuccessState(data)
|
||||
[file.name]: config.buildSuccessState(data),
|
||||
}));
|
||||
showNotification(t('auth_files.quota_refresh_success', { name: file.name }), 'success');
|
||||
} catch (err: unknown) {
|
||||
@@ -88,7 +94,7 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
const status = getStatusFromError(err);
|
||||
updateQuotaState((prev: Record<string, unknown>) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildErrorState(message, status)
|
||||
[file.name]: config.buildErrorState(message, status),
|
||||
}));
|
||||
showNotification(t('auth_files.quota_refresh_failed', { name: file.name, message }), 'error');
|
||||
}
|
||||
@@ -123,7 +129,7 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
) : quotaStatus === 'error' ? (
|
||||
<div className={styles.quotaError}>
|
||||
{t(`${config.i18nPrefix}.load_failed`, {
|
||||
message: quotaErrorMessage
|
||||
message: quotaErrorMessage,
|
||||
})}
|
||||
</div>
|
||||
) : quota ? (
|
||||
|
||||
@@ -24,7 +24,7 @@ export type AuthFileModelItem = {
|
||||
};
|
||||
export type AuthFileIconAsset = string | { light: string; dark: string };
|
||||
|
||||
export type QuotaProviderType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi';
|
||||
export type QuotaProviderType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi' | 'xai';
|
||||
|
||||
export const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>([
|
||||
'antigravity',
|
||||
@@ -32,6 +32,7 @@ export const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>([
|
||||
'codex',
|
||||
'gemini-cli',
|
||||
'kimi',
|
||||
'xai',
|
||||
]);
|
||||
|
||||
export const MIN_CARD_PAGE_SIZE = 3;
|
||||
@@ -247,7 +248,7 @@ export const formatModified = (item: AuthFileItem): string => {
|
||||
const date =
|
||||
Number.isFinite(asNumber) && !Number.isNaN(asNumber)
|
||||
? new Date(asNumber < 1e12 ? asNumber * 1000 : asNumber)
|
||||
: parseTimestamp(raw) ?? new Date(String(raw));
|
||||
: (parseTimestamp(raw) ?? new Date(String(raw)));
|
||||
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
|
||||
};
|
||||
|
||||
|
||||
@@ -726,6 +726,22 @@
|
||||
"limit_index": "Limit #{{index}}",
|
||||
"reset_hint": "resets in {{hint}}"
|
||||
},
|
||||
"xai_quota": {
|
||||
"title": "Grok Quota",
|
||||
"empty_title": "No xAI/Grok Auth Files",
|
||||
"empty_desc": "Log in with xAI OAuth to view Grok quota.",
|
||||
"idle": "Click here to refresh quota",
|
||||
"loading": "Loading quota...",
|
||||
"load_failed": "Failed to load quota: {{message}}",
|
||||
"missing_auth_index": "Auth file missing auth_index",
|
||||
"empty_data": "No quota data available",
|
||||
"refresh_button": "Refresh Quota",
|
||||
"fetch_all": "Fetch All",
|
||||
"monthly_credits": "Monthly credits",
|
||||
"pay_as_you_go_label": "Pay as you go",
|
||||
"pay_as_you_go_enabled": "Enabled, cap {{cap}}",
|
||||
"pay_as_you_go_disabled": "Disabled"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Vertex JSON Login",
|
||||
"description": "Upload a Google service account JSON to store it as auth-dir/vertex-<project>.json using the same rules as the CLI vertex-import helper.",
|
||||
|
||||
@@ -723,6 +723,22 @@
|
||||
"limit_index": "Лимит #{{index}}",
|
||||
"reset_hint": "сброс через {{hint}}"
|
||||
},
|
||||
"xai_quota": {
|
||||
"title": "Квота Grok",
|
||||
"empty_title": "Файлы авторизации xAI/Grok отсутствуют",
|
||||
"empty_desc": "Войдите через xAI OAuth, чтобы увидеть квоту Grok.",
|
||||
"idle": "Не загружено. Нажмите \"Обновить квоту\".",
|
||||
"loading": "Загрузка квоты...",
|
||||
"load_failed": "Не удалось загрузить квоту: {{message}}",
|
||||
"missing_auth_index": "В файле авторизации отсутствует auth_index",
|
||||
"empty_data": "Данные по квоте отсутствуют",
|
||||
"refresh_button": "Обновить квоту",
|
||||
"fetch_all": "Получить все",
|
||||
"monthly_credits": "Месячные кредиты",
|
||||
"pay_as_you_go_label": "Оплата по факту",
|
||||
"pay_as_you_go_enabled": "Включено, лимит {{cap}}",
|
||||
"pay_as_you_go_disabled": "Отключено"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Вход с Vertex JSON",
|
||||
"description": "Загрузите JSON ключа сервисного аккаунта Google, чтобы сохранить его как auth-dir/vertex-<project>.json по тем же правилам, что и помощник CLI vertex-import.",
|
||||
|
||||
@@ -726,6 +726,22 @@
|
||||
"limit_index": "限额 #{{index}}",
|
||||
"reset_hint": "{{hint}} 后重置"
|
||||
},
|
||||
"xai_quota": {
|
||||
"title": "Grok 额度",
|
||||
"empty_title": "暂无 xAI/Grok 认证",
|
||||
"empty_desc": "使用 xAI OAuth 登录后即可查看 Grok 额度。",
|
||||
"idle": "点击此处刷新额度",
|
||||
"loading": "正在加载额度...",
|
||||
"load_failed": "额度获取失败:{{message}}",
|
||||
"missing_auth_index": "认证文件缺少 auth_index",
|
||||
"empty_data": "暂无额度数据",
|
||||
"refresh_button": "刷新额度",
|
||||
"fetch_all": "获取全部",
|
||||
"monthly_credits": "月度积分",
|
||||
"pay_as_you_go_label": "按量付费",
|
||||
"pay_as_you_go_enabled": "已启用,封顶 {{cap}}",
|
||||
"pay_as_you_go_disabled": "未启用"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Vertex JSON 登录",
|
||||
"description": "上传 Google 服务账号 JSON,使用 CLI vertex-import 同步规则写入 auth-dir/vertex-<project>.json。",
|
||||
|
||||
@@ -726,6 +726,22 @@
|
||||
"limit_index": "限額 #{{index}}",
|
||||
"reset_hint": "{{hint}} 後重置"
|
||||
},
|
||||
"xai_quota": {
|
||||
"title": "Grok 配額",
|
||||
"empty_title": "暫無 xAI/Grok 驗證",
|
||||
"empty_desc": "使用 xAI OAuth 登入後即可查看 Grok 配額。",
|
||||
"idle": "點擊此處重新整理配額",
|
||||
"loading": "正在載入配額...",
|
||||
"load_failed": "配額取得失敗:{{message}}",
|
||||
"missing_auth_index": "驗證檔案缺少 auth_index",
|
||||
"empty_data": "暫無配額資料",
|
||||
"refresh_button": "重新整理配額",
|
||||
"fetch_all": "取得全部",
|
||||
"monthly_credits": "月度點數",
|
||||
"pay_as_you_go_label": "按量付費",
|
||||
"pay_as_you_go_enabled": "已啟用,封頂 {{cap}}",
|
||||
"pay_as_you_go_disabled": "未啟用"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Vertex JSON 登入",
|
||||
"description": "上傳 Google 服務帳號 JSON,使用 CLI vertex-import 同步規則寫入 auth-dir/vertex-<project>.json。",
|
||||
|
||||
@@ -517,6 +517,10 @@
|
||||
background-image: linear-gradient(180deg, rgba(220, 232, 255, 0.08), transparent);
|
||||
}
|
||||
|
||||
.xaiCard {
|
||||
background-image: linear-gradient(180deg, rgba(243, 244, 246, 0.08), transparent);
|
||||
}
|
||||
|
||||
.quotaSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
.claudeGrid,
|
||||
.codexGrid,
|
||||
.geminiCliGrid,
|
||||
.kimiGrid {
|
||||
.kimiGrid,
|
||||
.xaiGrid {
|
||||
display: grid;
|
||||
gap: $spacing-md;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
@@ -128,7 +129,8 @@
|
||||
.claudeControls,
|
||||
.codexControls,
|
||||
.geminiCliControls,
|
||||
.kimiControls {
|
||||
.kimiControls,
|
||||
.xaiControls {
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
flex-wrap: wrap;
|
||||
@@ -140,7 +142,8 @@
|
||||
.claudeControl,
|
||||
.codexControl,
|
||||
.geminiCliControl,
|
||||
.kimiControl {
|
||||
.kimiControl,
|
||||
.xaiControl {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -247,33 +250,27 @@
|
||||
|
||||
// 卡片渐变背景 — 基于 TYPE_COLORS light.bg 转 rgba
|
||||
.claudeCard {
|
||||
background-image: linear-gradient(180deg,
|
||||
rgba(251, 236, 228, 0.18),
|
||||
rgba(251, 236, 228, 0));
|
||||
background-image: linear-gradient(180deg, rgba(251, 236, 228, 0.18), rgba(251, 236, 228, 0));
|
||||
}
|
||||
|
||||
.antigravityCard {
|
||||
background-image: linear-gradient(180deg,
|
||||
rgba(224, 247, 250, 0.12),
|
||||
rgba(224, 247, 250, 0));
|
||||
background-image: linear-gradient(180deg, rgba(224, 247, 250, 0.12), rgba(224, 247, 250, 0));
|
||||
}
|
||||
|
||||
.codexCard {
|
||||
background-image: linear-gradient(180deg,
|
||||
rgba(234, 231, 255, 0.18),
|
||||
rgba(234, 231, 255, 0));
|
||||
background-image: linear-gradient(180deg, rgba(234, 231, 255, 0.18), rgba(234, 231, 255, 0));
|
||||
}
|
||||
|
||||
.geminiCliCard {
|
||||
background-image: linear-gradient(180deg,
|
||||
rgba(224, 232, 255, 0.2),
|
||||
rgba(224, 232, 255, 0));
|
||||
background-image: linear-gradient(180deg, rgba(224, 232, 255, 0.2), rgba(224, 232, 255, 0));
|
||||
}
|
||||
|
||||
.kimiCard {
|
||||
background-image: linear-gradient(180deg,
|
||||
rgba(220, 232, 255, 0.2),
|
||||
rgba(220, 232, 255, 0));
|
||||
background-image: linear-gradient(180deg, rgba(220, 232, 255, 0.2), rgba(220, 232, 255, 0));
|
||||
}
|
||||
|
||||
.xaiCard {
|
||||
background-image: linear-gradient(180deg, rgba(243, 244, 246, 0.22), rgba(243, 244, 246, 0));
|
||||
}
|
||||
|
||||
.quotaSection {
|
||||
@@ -636,7 +633,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
transition: transform $transition-fast, box-shadow $transition-fast, border-color $transition-fast;
|
||||
transition:
|
||||
transform $transition-fast,
|
||||
box-shadow $transition-fast,
|
||||
border-color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
CLAUDE_CONFIG,
|
||||
CODEX_CONFIG,
|
||||
GEMINI_CLI_CONFIG,
|
||||
KIMI_CONFIG
|
||||
KIMI_CONFIG,
|
||||
XAI_CONFIG,
|
||||
} from '@/components/quota';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import styles from './QuotaPage.module.scss';
|
||||
@@ -89,6 +90,12 @@ export function QuotaPage() {
|
||||
loading={loading}
|
||||
disabled={disableControls}
|
||||
/>
|
||||
<QuotaSection
|
||||
config={XAI_CONFIG}
|
||||
files={files}
|
||||
loading={loading}
|
||||
disabled={disableControls}
|
||||
/>
|
||||
<QuotaSection
|
||||
config={GEMINI_CLI_CONFIG}
|
||||
files={files}
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { AntigravityQuotaState, ClaudeQuotaState, CodexQuotaState, GeminiCliQuotaState, KimiQuotaState } from '@/types';
|
||||
import type {
|
||||
AntigravityQuotaState,
|
||||
ClaudeQuotaState,
|
||||
CodexQuotaState,
|
||||
GeminiCliQuotaState,
|
||||
KimiQuotaState,
|
||||
XaiQuotaState,
|
||||
} from '@/types';
|
||||
|
||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||
|
||||
@@ -13,15 +20,17 @@ interface QuotaStoreState {
|
||||
codexQuota: Record<string, CodexQuotaState>;
|
||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||
kimiQuota: Record<string, KimiQuotaState>;
|
||||
xaiQuota: Record<string, XaiQuotaState>;
|
||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||
setClaudeQuota: (updater: QuotaUpdater<Record<string, ClaudeQuotaState>>) => void;
|
||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||
setKimiQuota: (updater: QuotaUpdater<Record<string, KimiQuotaState>>) => void;
|
||||
setXaiQuota: (updater: QuotaUpdater<Record<string, XaiQuotaState>>) => void;
|
||||
clearQuotaCache: () => void;
|
||||
}
|
||||
|
||||
const resolveUpdater = <T,>(updater: QuotaUpdater<T>, prev: T): T => {
|
||||
const resolveUpdater = <T>(updater: QuotaUpdater<T>, prev: T): T => {
|
||||
if (typeof updater === 'function') {
|
||||
return (updater as (value: T) => T)(prev);
|
||||
}
|
||||
@@ -34,25 +43,30 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
||||
codexQuota: {},
|
||||
geminiCliQuota: {},
|
||||
kimiQuota: {},
|
||||
xaiQuota: {},
|
||||
setAntigravityQuota: (updater) =>
|
||||
set((state) => ({
|
||||
antigravityQuota: resolveUpdater(updater, state.antigravityQuota)
|
||||
antigravityQuota: resolveUpdater(updater, state.antigravityQuota),
|
||||
})),
|
||||
setClaudeQuota: (updater) =>
|
||||
set((state) => ({
|
||||
claudeQuota: resolveUpdater(updater, state.claudeQuota)
|
||||
claudeQuota: resolveUpdater(updater, state.claudeQuota),
|
||||
})),
|
||||
setCodexQuota: (updater) =>
|
||||
set((state) => ({
|
||||
codexQuota: resolveUpdater(updater, state.codexQuota)
|
||||
codexQuota: resolveUpdater(updater, state.codexQuota),
|
||||
})),
|
||||
setGeminiCliQuota: (updater) =>
|
||||
set((state) => ({
|
||||
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota)
|
||||
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota),
|
||||
})),
|
||||
setKimiQuota: (updater) =>
|
||||
set((state) => ({
|
||||
kimiQuota: resolveUpdater(updater, state.kimiQuota)
|
||||
kimiQuota: resolveUpdater(updater, state.kimiQuota),
|
||||
})),
|
||||
setXaiQuota: (updater) =>
|
||||
set((state) => ({
|
||||
xaiQuota: resolveUpdater(updater, state.xaiQuota),
|
||||
})),
|
||||
clearQuotaCache: () =>
|
||||
set({
|
||||
@@ -60,6 +74,7 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
||||
claudeQuota: {},
|
||||
codexQuota: {},
|
||||
geminiCliQuota: {},
|
||||
kimiQuota: {}
|
||||
})
|
||||
kimiQuota: {},
|
||||
xaiQuota: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -306,3 +306,40 @@ export interface KimiQuotaState {
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
}
|
||||
|
||||
// xAI/Grok API payload types
|
||||
export interface XaiBillingCent {
|
||||
val?: number | string;
|
||||
}
|
||||
|
||||
export interface XaiBillingConfig {
|
||||
monthlyLimit?: XaiBillingCent | number | string | null;
|
||||
monthly_limit?: XaiBillingCent | number | string | null;
|
||||
used?: XaiBillingCent | number | string | null;
|
||||
onDemandCap?: XaiBillingCent | number | string | null;
|
||||
on_demand_cap?: XaiBillingCent | number | string | null;
|
||||
billingPeriodStart?: string;
|
||||
billing_period_start?: string;
|
||||
billingPeriodEnd?: string;
|
||||
billing_period_end?: string;
|
||||
}
|
||||
|
||||
export interface XaiBillingPayload {
|
||||
config?: XaiBillingConfig | null;
|
||||
}
|
||||
|
||||
export interface XaiBillingSummary {
|
||||
monthlyLimitCents: number | null;
|
||||
usedCents: number | null;
|
||||
onDemandCapCents: number | null;
|
||||
billingPeriodStart?: string;
|
||||
billingPeriodEnd?: string;
|
||||
usedPercent: number | null;
|
||||
}
|
||||
|
||||
export interface XaiQuotaState {
|
||||
status: 'idle' | 'loading' | 'success' | 'error';
|
||||
billing: XaiBillingSummary | null;
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ export const TYPE_COLORS: Record<string, TypeColorSet> = {
|
||||
light: { bg: '#e0f7fa', text: '#006064' },
|
||||
dark: { bg: '#004d40', text: '#80deea' },
|
||||
},
|
||||
xai: {
|
||||
light: { bg: '#f3f4f6', text: '#111827', border: '1px solid #d1d5db' },
|
||||
dark: { bg: '#111827', text: '#f9fafb', border: '1px solid #374151' },
|
||||
},
|
||||
iflow: {
|
||||
light: { bg: '#f5e3fc', text: '#9025c8' },
|
||||
dark: { bg: '#521490', text: '#d49cf5' },
|
||||
@@ -176,7 +180,11 @@ export const CLAUDE_REQUEST_HEADERS = {
|
||||
export const CLAUDE_USAGE_WINDOW_KEYS = [
|
||||
{ key: 'five_hour', id: 'five-hour', labelKey: 'claude_quota.five_hour' },
|
||||
{ key: 'seven_day', id: 'seven-day', labelKey: 'claude_quota.seven_day' },
|
||||
{ key: 'seven_day_oauth_apps', id: 'seven-day-oauth-apps', labelKey: 'claude_quota.seven_day_oauth_apps' },
|
||||
{
|
||||
key: 'seven_day_oauth_apps',
|
||||
id: 'seven-day-oauth-apps',
|
||||
labelKey: 'claude_quota.seven_day_oauth_apps',
|
||||
},
|
||||
{ key: 'seven_day_opus', id: 'seven-day-opus', labelKey: 'claude_quota.seven_day_opus' },
|
||||
{ key: 'seven_day_sonnet', id: 'seven-day-sonnet', labelKey: 'claude_quota.seven_day_sonnet' },
|
||||
{ key: 'seven_day_cowork', id: 'seven-day-cowork', labelKey: 'claude_quota.seven_day_cowork' },
|
||||
@@ -198,3 +206,10 @@ export const KIMI_USAGE_URL = 'https://api.kimi.com/coding/v1/usages';
|
||||
export const KIMI_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
};
|
||||
|
||||
// xAI/Grok API configuration
|
||||
export const XAI_BILLING_URL = 'https://cli-chat-proxy.grok.com/v1/billing';
|
||||
|
||||
export const XAI_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
};
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
* Normalization and parsing functions for quota data.
|
||||
*/
|
||||
|
||||
import type { ClaudeUsagePayload, CodexUsagePayload, GeminiCliCodeAssistPayload, GeminiCliQuotaPayload, KimiUsagePayload } from '@/types';
|
||||
import type {
|
||||
ClaudeUsagePayload,
|
||||
CodexUsagePayload,
|
||||
GeminiCliCodeAssistPayload,
|
||||
GeminiCliQuotaPayload,
|
||||
KimiUsagePayload,
|
||||
XaiBillingPayload,
|
||||
} from '@/types';
|
||||
import { normalizeAuthIndex } from '@/utils/authIndex';
|
||||
|
||||
const GEMINI_CLI_MODEL_SUFFIX = '_vertex';
|
||||
@@ -191,7 +198,9 @@ export function parseGeminiCliQuotaPayload(payload: unknown): GeminiCliQuotaPayl
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseGeminiCliCodeAssistPayload(payload: unknown): GeminiCliCodeAssistPayload | null {
|
||||
export function parseGeminiCliCodeAssistPayload(
|
||||
payload: unknown
|
||||
): GeminiCliCodeAssistPayload | null {
|
||||
if (payload === undefined || payload === null) return null;
|
||||
if (typeof payload === 'string') {
|
||||
const trimmed = payload.trim();
|
||||
@@ -224,3 +233,20 @@ export function parseKimiUsagePayload(payload: unknown): KimiUsagePayload | null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseXaiBillingPayload(payload: unknown): XaiBillingPayload | null {
|
||||
if (payload === undefined || payload === null) return null;
|
||||
if (typeof payload === 'string') {
|
||||
const trimmed = payload.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as XaiBillingPayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof payload === 'object') {
|
||||
return payload as XaiBillingPayload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { GEMINI_CLI_IGNORED_MODEL_PREFIXES } from './constants';
|
||||
|
||||
export function resolveAuthProvider(file: AuthFileItem): string {
|
||||
const raw = file.provider ?? file.type ?? '';
|
||||
return String(raw).trim().toLowerCase();
|
||||
const key = String(raw).trim().toLowerCase().replace(/_/g, '-');
|
||||
if (key === 'x-ai' || key === 'grok') return 'xai';
|
||||
return key;
|
||||
}
|
||||
|
||||
export function isAntigravityFile(file: AuthFileItem): boolean {
|
||||
@@ -25,9 +27,7 @@ export function isClaudeOAuthFile(file: AuthFileItem): boolean {
|
||||
? (file.metadata as Record<string, unknown>)
|
||||
: null;
|
||||
const accessToken =
|
||||
metadata && typeof metadata.access_token === 'string'
|
||||
? metadata.access_token.trim()
|
||||
: '';
|
||||
metadata && typeof metadata.access_token === 'string' ? metadata.access_token.trim() : '';
|
||||
return accessToken.includes('sk-ant-oat');
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ export function isKimiFile(file: AuthFileItem): boolean {
|
||||
return resolveAuthProvider(file) === 'kimi';
|
||||
}
|
||||
|
||||
export function isXaiFile(file: AuthFileItem): boolean {
|
||||
return resolveAuthProvider(file) === 'xai';
|
||||
}
|
||||
|
||||
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
||||
const raw = file['runtime_only'] ?? file.runtimeOnly;
|
||||
if (typeof raw === 'boolean') return raw;
|
||||
|
||||
Reference in New Issue
Block a user