mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 16:16:52 +00:00
refactor(quota): simplify AccountTier to free|paid|unknown
- consolidate 'pro'/'ultra' into single 'paid' tier (no distinction needed) - update mapTierString() and inferTierFromModels() in quota-fetcher.ts - fix tier_priority default from ['ultra','pro'] to ['paid'] - add missing quota_management to mergeWithDefaults() in config-loader - update UI tier badge styling for 'paid' tier - update api-client.ts tier type definition
This commit is contained in:
@@ -14,8 +14,8 @@ import { CLIProxyProvider } from './types';
|
|||||||
import { getCliproxyDir, getAuthDir } from './config-generator';
|
import { getCliproxyDir, getAuthDir } from './config-generator';
|
||||||
import { PROVIDER_TYPE_VALUES } from './auth/auth-types';
|
import { PROVIDER_TYPE_VALUES } from './auth/auth-types';
|
||||||
|
|
||||||
/** Account tier for quota management */
|
/** Account tier for quota management (free vs paid - no Pro/Ultra distinction needed) */
|
||||||
export type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown';
|
export type AccountTier = 'free' | 'paid' | 'unknown';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Providers that typically have empty email in OAuth token files.
|
* Providers that typically have empty email in OAuth token files.
|
||||||
@@ -45,7 +45,7 @@ export interface AccountInfo {
|
|||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
/** ISO timestamp when paused */
|
/** ISO timestamp when paused */
|
||||||
pausedAt?: string;
|
pausedAt?: string;
|
||||||
/** Account tier: free, pro, ultra */
|
/** Account tier: free or paid (Pro/Ultra combined) */
|
||||||
tier?: AccountTier;
|
tier?: AccountTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ interface TokenRefreshResponse {
|
|||||||
/** Tier info from loadCodeAssist */
|
/** Tier info from loadCodeAssist */
|
||||||
interface TierInfo {
|
interface TierInfo {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** loadCodeAssist response */
|
/** loadCodeAssist response */
|
||||||
@@ -123,6 +124,8 @@ interface LoadCodeAssistResponse {
|
|||||||
currentTier?: TierInfo;
|
currentTier?: TierInfo;
|
||||||
/** Paid tier (reflects actual subscription - takes priority) */
|
/** Paid tier (reflects actual subscription - takes priority) */
|
||||||
paidTier?: TierInfo;
|
paidTier?: TierInfo;
|
||||||
|
/** Array of allowed tiers - use isDefault=true to find active tier (CLIProxyAPIPlus approach) */
|
||||||
|
allowedTiers?: TierInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** fetchAvailableModels response model */
|
/** fetchAvailableModels response model */
|
||||||
@@ -284,22 +287,49 @@ function readAuthData(provider: CLIProxyProvider, accountId: string): AuthData |
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map API tier string to AccountTier type
|
* Map tier ID string to AccountTier type
|
||||||
* API returns tier IDs like: "standard-tier" (free), "pro-tier" (pro), "ultra-tier" (ultra)
|
* Simplified: anything with 'pro' or 'ultra' = paid, 'free'/'legacy' = free
|
||||||
* Also handles legacy formats: "FREE", "PRO", "ULTRA"
|
|
||||||
*/
|
*/
|
||||||
function mapTierString(tierStr: string | undefined): AccountTier {
|
function mapTierString(tierStr: string | undefined): AccountTier {
|
||||||
if (!tierStr) return 'unknown';
|
if (!tierStr) return 'unknown';
|
||||||
const normalized = tierStr.toLowerCase();
|
const normalized = tierStr.toLowerCase();
|
||||||
if (normalized.includes('ultra')) return 'ultra';
|
if (normalized.includes('ultra') || normalized.includes('pro')) return 'paid';
|
||||||
if (normalized.includes('pro')) return 'pro';
|
if (normalized.includes('free') || normalized.includes('legacy')) {
|
||||||
// "standard-tier" or "free" both map to free
|
return 'free';
|
||||||
if (normalized.includes('standard') || normalized.includes('free')) return 'free';
|
}
|
||||||
|
// "standard-tier" and other unknown values should NOT map to 'free'
|
||||||
|
// Let inferTierFromModels handle the detection
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get project ID and subscription tier via loadCodeAssist endpoint
|
* Infer tier from model access patterns.
|
||||||
|
* - Paid: Has access to Claude models OR premium Gemini models
|
||||||
|
* - Free: Only basic models
|
||||||
|
*
|
||||||
|
* Claude models are Ultra-exclusive, premium Gemini indicates Pro/Ultra.
|
||||||
|
* Both are "paid" tier for our purposes.
|
||||||
|
*/
|
||||||
|
function inferTierFromModels(models: ModelQuota[]): AccountTier {
|
||||||
|
if (models.length === 0) return 'unknown';
|
||||||
|
|
||||||
|
// Check for Claude models (Ultra-exclusive) or premium Gemini (Pro/Ultra)
|
||||||
|
const hasPaidAccess = models.some((m) => {
|
||||||
|
const name = m.name.toLowerCase();
|
||||||
|
return (
|
||||||
|
name.includes('claude') ||
|
||||||
|
name.includes('gemini-3-pro') ||
|
||||||
|
name.includes('gemini-2.5-pro') ||
|
||||||
|
name.includes('gemini-pro-high')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasPaidAccess ? 'paid' : 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get project ID and tier via loadCodeAssist endpoint
|
||||||
|
* Uses allowedTiers array with isDefault=true (CLIProxyAPIPlus approach)
|
||||||
*/
|
*/
|
||||||
async function getProjectId(accessToken: string): Promise<{
|
async function getProjectId(accessToken: string): Promise<{
|
||||||
projectId: string | null;
|
projectId: string | null;
|
||||||
@@ -361,8 +391,17 @@ async function getProjectId(accessToken: string): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract tier: priority paidTier > currentTier (paid reflects actual subscription)
|
// Extract tier using CLIProxyAPIPlus approach:
|
||||||
const tierStr = data.paidTier?.id || data.currentTier?.id;
|
// 1. Find tier with isDefault=true in allowedTiers array
|
||||||
|
// 2. Fallback to paidTier > currentTier
|
||||||
|
let tierStr: string | undefined;
|
||||||
|
if (data.allowedTiers && Array.isArray(data.allowedTiers)) {
|
||||||
|
const defaultTier = data.allowedTiers.find((t) => t.isDefault);
|
||||||
|
tierStr = defaultTier?.id;
|
||||||
|
}
|
||||||
|
if (!tierStr) {
|
||||||
|
tierStr = data.paidTier?.id || data.currentTier?.id;
|
||||||
|
}
|
||||||
const tier = mapTierString(tierStr);
|
const tier = mapTierString(tierStr);
|
||||||
|
|
||||||
return { projectId: projectId.trim(), tier };
|
return { projectId: projectId.trim(), tier };
|
||||||
@@ -569,21 +608,29 @@ export async function fetchAccountQuota(
|
|||||||
refreshResult.accessToken,
|
refreshResult.accessToken,
|
||||||
projectId as string
|
projectId as string
|
||||||
);
|
);
|
||||||
// Use API tier (from loadCodeAssist) instead of model-based detection
|
// Determine tier: model access (Claude = Ultra) > API tier > fallback
|
||||||
if (retryResult.success) {
|
if (retryResult.success) {
|
||||||
retryResult.tier = apiTier;
|
let finalTier = inferTierFromModels(retryResult.models);
|
||||||
|
if (finalTier === 'unknown') {
|
||||||
|
finalTier = apiTier !== 'unknown' ? apiTier : 'paid';
|
||||||
|
}
|
||||||
|
retryResult.tier = finalTier;
|
||||||
retryResult.accountId = accountId;
|
retryResult.accountId = accountId;
|
||||||
setAccountTier(provider, accountId, apiTier);
|
setAccountTier(provider, accountId, finalTier);
|
||||||
}
|
}
|
||||||
return retryResult;
|
return retryResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use API tier (from loadCodeAssist) instead of model-based detection
|
// Determine tier: model access > API tier > fallback to paid
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
result.tier = apiTier;
|
let finalTier = inferTierFromModels(result.models);
|
||||||
|
if (finalTier === 'unknown') {
|
||||||
|
finalTier = apiTier !== 'unknown' ? apiTier : 'paid';
|
||||||
|
}
|
||||||
|
result.tier = finalTier;
|
||||||
result.accountId = accountId;
|
result.accountId = accountId;
|
||||||
setAccountTier(provider, accountId, apiTier);
|
setAccountTier(provider, accountId, finalTier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export async function findHealthyAccount(
|
|||||||
exclude: string[]
|
exclude: string[]
|
||||||
): Promise<{ id: string; tier: string; lastQuota: number } | null> {
|
): Promise<{ id: string; tier: string; lastQuota: number } | null> {
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
const tierPriority = config.quota_management?.auto?.tier_priority ?? ['ultra', 'pro'];
|
const tierPriority = config.quota_management?.auto?.tier_priority ?? ['paid'];
|
||||||
const threshold = config.quota_management?.auto?.exhaustion_threshold ?? 5;
|
const threshold = config.quota_management?.auto?.exhaustion_threshold ?? 5;
|
||||||
|
|
||||||
const accounts = getProviderAccounts(provider);
|
const accounts = getProviderAccounts(provider);
|
||||||
@@ -224,7 +224,7 @@ export async function findHealthyAccount(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
tier: account.tier || 'pro',
|
tier: account.tier || 'paid',
|
||||||
lastQuota: avgQuota,
|
lastQuota: avgQuota,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
DEFAULT_COPILOT_CONFIG,
|
DEFAULT_COPILOT_CONFIG,
|
||||||
DEFAULT_GLOBAL_ENV,
|
DEFAULT_GLOBAL_ENV,
|
||||||
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG,
|
||||||
GlobalEnvConfig,
|
GlobalEnvConfig,
|
||||||
} from './unified-config-types';
|
} from './unified-config-types';
|
||||||
import { isUnifiedConfigEnabled } from './feature-flags';
|
import { isUnifiedConfigEnabled } from './feature-flags';
|
||||||
@@ -212,6 +213,35 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
DEFAULT_CLIPROXY_SERVER_CONFIG.local.auto_start,
|
DEFAULT_CLIPROXY_SERVER_CONFIG.local.auto_start,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Quota management config - hybrid auto+manual account selection
|
||||||
|
quota_management: {
|
||||||
|
mode: partial.quota_management?.mode ?? DEFAULT_QUOTA_MANAGEMENT_CONFIG.mode,
|
||||||
|
auto: {
|
||||||
|
preflight_check:
|
||||||
|
partial.quota_management?.auto?.preflight_check ??
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.preflight_check,
|
||||||
|
exhaustion_threshold:
|
||||||
|
partial.quota_management?.auto?.exhaustion_threshold ??
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.exhaustion_threshold,
|
||||||
|
tier_priority:
|
||||||
|
partial.quota_management?.auto?.tier_priority ??
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.tier_priority,
|
||||||
|
cooldown_minutes:
|
||||||
|
partial.quota_management?.auto?.cooldown_minutes ??
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.cooldown_minutes,
|
||||||
|
},
|
||||||
|
manual: {
|
||||||
|
paused_accounts:
|
||||||
|
partial.quota_management?.manual?.paused_accounts ??
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.paused_accounts,
|
||||||
|
forced_default:
|
||||||
|
partial.quota_management?.manual?.forced_default ??
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.forced_default,
|
||||||
|
tier_lock:
|
||||||
|
partial.quota_management?.manual?.tier_lock ??
|
||||||
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.tier_lock,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ export interface AutoQuotaConfig {
|
|||||||
preflight_check: boolean;
|
preflight_check: boolean;
|
||||||
/** Quota percentage below which account is "exhausted" (default: 5) */
|
/** Quota percentage below which account is "exhausted" (default: 5) */
|
||||||
exhaustion_threshold: number;
|
exhaustion_threshold: number;
|
||||||
/** Tier priority for failover, highest to lowest (default: ['ultra', 'pro']) */
|
/** Tier priority for failover, highest to lowest (default: ['paid']) */
|
||||||
tier_priority: string[];
|
tier_priority: string[];
|
||||||
/** Minutes to skip exhausted account before retry (default: 5) */
|
/** Minutes to skip exhausted account before retry (default: 5) */
|
||||||
cooldown_minutes: number;
|
cooldown_minutes: number;
|
||||||
@@ -402,7 +402,7 @@ export interface QuotaManagementConfig {
|
|||||||
export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = {
|
export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = {
|
||||||
preflight_check: true,
|
preflight_check: true,
|
||||||
exhaustion_threshold: 5,
|
exhaustion_threshold: 5,
|
||||||
tier_priority: ['ultra', 'pro'],
|
tier_priority: ['paid'],
|
||||||
cooldown_minutes: 5,
|
cooldown_minutes: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,7 @@ export function AccountItem({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[10px] h-4 px-1.5 uppercase',
|
'text-[10px] h-4 px-1.5 uppercase',
|
||||||
account.tier === 'ultra' && 'border-purple-500 text-purple-600',
|
account.tier === 'paid' && 'border-blue-500 text-blue-600',
|
||||||
account.tier === 'pro' && 'border-blue-500 text-blue-600',
|
|
||||||
account.tier === 'free' && 'border-gray-400 text-gray-500'
|
account.tier === 'free' && 'border-gray-400 text-gray-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ export interface OAuthAccount {
|
|||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
/** ISO timestamp when account was paused */
|
/** ISO timestamp when account was paused */
|
||||||
pausedAt?: string;
|
pausedAt?: string;
|
||||||
/** Account tier: free, pro, ultra */
|
/** Account tier: free or paid (Pro/Ultra combined) */
|
||||||
tier?: 'free' | 'pro' | 'ultra' | 'unknown';
|
tier?: 'free' | 'paid' | 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthStatus {
|
export interface AuthStatus {
|
||||||
|
|||||||
Reference in New Issue
Block a user