mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 22:15:33 +00:00
fix(cliproxy): refresh non-default gemini quota tokens
This commit is contained in:
1 parent
04878f0864
commit
b6fb443f57
11 files changed
+207
-71
No files matched your search
@@ -16,12 +16,35 @@ export function sanitizeEmail(email: string): string {
|
||||
* Check if token is expired based on the expired timestamp.
|
||||
* Returns false if timestamp is missing or invalid (fail-open for quota display).
|
||||
*/
|
||||
export function isTokenExpired(expiredStr?: string): boolean {
|
||||
if (!expiredStr) return false;
|
||||
export function getTokenExpiryTimestamp(expiredValue?: string | number | null): number | null {
|
||||
if (expiredValue === undefined || expiredValue === null || expiredValue === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const expiredDate = new Date(expiredStr);
|
||||
return expiredDate.getTime() < Date.now();
|
||||
if (typeof expiredValue === 'number') {
|
||||
return Number.isFinite(expiredValue) ? expiredValue : null;
|
||||
}
|
||||
|
||||
const trimmed = expiredValue.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(trimmed)) {
|
||||
const numericTimestamp = Number(trimmed);
|
||||
return Number.isFinite(numericTimestamp) ? numericTimestamp : null;
|
||||
}
|
||||
|
||||
const expiredDate = new Date(trimmed);
|
||||
const expiredAt = expiredDate.getTime();
|
||||
return Number.isNaN(expiredAt) ? null : expiredAt;
|
||||
} catch {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTokenExpired(expiredValue?: string | number | null): boolean {
|
||||
const expiredAt = getTokenExpiryTimestamp(expiredValue);
|
||||
return expiredAt !== null ? expiredAt < Date.now() : false;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { getAuthDir } from './config-generator';
|
||||
import { getProviderAccounts, getPausedDir } from './account-manager';
|
||||
import { sanitizeEmail, isTokenExpired } from './auth-utils';
|
||||
import { getTokenExpiryTimestamp, sanitizeEmail, isTokenExpired } from './auth-utils';
|
||||
import { refreshGeminiToken } from './auth/gemini-token-refresh';
|
||||
import {
|
||||
buildGeminiCliBucketsFromParsedBuckets,
|
||||
@@ -38,7 +38,7 @@ interface GeminiCliAuthData {
|
||||
accessToken: string;
|
||||
projectId: string | null;
|
||||
isExpired: boolean;
|
||||
expiresAt: string | null;
|
||||
expiresAt: string | number | null;
|
||||
}
|
||||
|
||||
/** Raw bucket from API response */
|
||||
@@ -130,17 +130,23 @@ function extractAccessToken(data: Record<string, unknown>): string | null {
|
||||
* Extract expiry from Gemini auth file data
|
||||
* Handles both flat (expired) and nested (token.expiry) structures
|
||||
*/
|
||||
function extractExpiry(data: Record<string, unknown>): string | null {
|
||||
function extractExpiry(data: Record<string, unknown>): string | number | null {
|
||||
// Flat structure: { expired: "..." }
|
||||
if (typeof data.expired === 'string') {
|
||||
return data.expired;
|
||||
}
|
||||
if (typeof data.expired === 'number') {
|
||||
return data.expired;
|
||||
}
|
||||
// Nested structure: { token: { expiry: "..." } }
|
||||
if (data.token && typeof data.token === 'object') {
|
||||
const token = data.token as Record<string, unknown>;
|
||||
if (typeof token.expiry === 'string') {
|
||||
return token.expiry;
|
||||
}
|
||||
if (typeof token.expiry === 'number') {
|
||||
return token.expiry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -736,19 +742,20 @@ export async function fetchGeminiCliQuota(
|
||||
|
||||
// Proactive refresh: refresh if expired OR expiring within 5 minutes
|
||||
const REFRESH_LEAD_TIME_MS = 5 * 60 * 1000;
|
||||
const expiresAt = getTokenExpiryTimestamp(authData.expiresAt);
|
||||
const shouldRefresh =
|
||||
authData.isExpired ||
|
||||
!authData.expiresAt ||
|
||||
new Date(authData.expiresAt).getTime() - Date.now() < REFRESH_LEAD_TIME_MS;
|
||||
authData.isExpired || expiresAt === null || expiresAt - Date.now() < REFRESH_LEAD_TIME_MS;
|
||||
let attemptedRefresh = false;
|
||||
|
||||
if (shouldRefresh) {
|
||||
attemptedRefresh = true;
|
||||
if (verbose)
|
||||
console.error(
|
||||
authData.isExpired
|
||||
? '[i] Token expired, refreshing...'
|
||||
: '[i] Token expiring soon, proactive refresh...'
|
||||
);
|
||||
const refreshResult = await refreshGeminiToken();
|
||||
const refreshResult = await refreshGeminiToken(accountId);
|
||||
|
||||
if (refreshResult.success) {
|
||||
if (verbose) console.error('[i] Token refreshed successfully');
|
||||
@@ -776,10 +783,10 @@ export async function fetchGeminiCliQuota(
|
||||
// First attempt with current token
|
||||
const result = await fetchWithAuthData(authData, accountId, verbose);
|
||||
|
||||
// If 401 error and we haven't refreshed yet, try refresh and retry
|
||||
if (result.needsReauth && result.error?.includes('expired')) {
|
||||
// Retry once with an account-scoped refresh when the quota endpoint rejects auth.
|
||||
if (result.needsReauth && !attemptedRefresh) {
|
||||
if (verbose) console.error('[i] Got 401, attempting refresh and retry...');
|
||||
const refreshResult = await refreshGeminiToken();
|
||||
const refreshResult = await refreshGeminiToken(accountId);
|
||||
if (refreshResult.success) {
|
||||
const refreshedAuthData = readGeminiCliAuthData(accountId);
|
||||
if (refreshedAuthData) {
|
||||
|
||||
Reference in new issue
Block a user