mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 22:16:41 +00:00
- Add PROVIDER_ERROR_PATTERNS for HTTP 4xx/5xx, overloaded, quota, rate limit - Add detectFailedTier() to identify which composite tier failed from stderr - Add isProviderError() to distinguish provider failures from normal exits
129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
/**
|
|
* Retry Handler - Error recovery and retry logic
|
|
*
|
|
* Handles:
|
|
* - Network error detection
|
|
* - Token expiration handling
|
|
* - Quota management
|
|
* - Account switching
|
|
*/
|
|
|
|
import { fail, warn, info } from '../../utils/ui';
|
|
import { CLIProxyProvider } from '../types';
|
|
import { handleBanDetection } from '../account-safety';
|
|
import { CompositeTierConfig } from '../../config/unified-config-types';
|
|
|
|
/**
|
|
* Check if error is network-related
|
|
*/
|
|
export function isNetworkError(error: Error): boolean {
|
|
const networkErrors = [
|
|
'getaddrinfo',
|
|
'ENOTFOUND',
|
|
'ETIMEDOUT',
|
|
'ECONNREFUSED',
|
|
'ENETUNREACH',
|
|
'EAI_AGAIN',
|
|
];
|
|
return networkErrors.some((errCode) => error.message.includes(errCode));
|
|
}
|
|
|
|
/**
|
|
* Handle network error with user-friendly message
|
|
*/
|
|
export function handleNetworkError(_error: Error): never {
|
|
console.error('');
|
|
console.error(fail('No network connection detected'));
|
|
console.error('');
|
|
console.error('CLIProxy binary download requires internet access.');
|
|
console.error('Please check your network connection and try again.');
|
|
console.error('');
|
|
process.exit(1);
|
|
}
|
|
|
|
/**
|
|
* Handle token expiration
|
|
*/
|
|
export async function handleTokenExpiration(
|
|
provider: CLIProxyProvider,
|
|
verbose: boolean
|
|
): Promise<void> {
|
|
const { ensureTokenValid } = await import('../auth/token-manager');
|
|
const tokenResult = await ensureTokenValid(provider, verbose);
|
|
|
|
if (!tokenResult.valid) {
|
|
// Check if this is an account ban/disable before generic error
|
|
if (tokenResult.error) {
|
|
const { getDefaultAccount } = await import('../account-manager');
|
|
const account = getDefaultAccount(provider);
|
|
if (account) {
|
|
handleBanDetection(provider, account.id, tokenResult.error);
|
|
}
|
|
}
|
|
|
|
// Token expired and refresh failed - trigger re-auth
|
|
console.error(warn('OAuth token expired and refresh failed'));
|
|
if (tokenResult.error) {
|
|
console.error(` ${tokenResult.error}`);
|
|
}
|
|
console.error(` Run "ccs ${provider} --auth" to re-authenticate`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (tokenResult.refreshed && verbose) {
|
|
console.error('[cliproxy] Token was refreshed proactively');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle quota check and auto-switching for Antigravity
|
|
*/
|
|
export async function handleQuotaCheck(provider: CLIProxyProvider): Promise<void> {
|
|
if (provider !== 'agy') return;
|
|
|
|
const { preflightCheck } = await import('../quota-manager');
|
|
const preflight = await preflightCheck(provider);
|
|
|
|
if (!preflight.proceed) {
|
|
console.error(fail(`Cannot start session: ${preflight.reason}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (preflight.switchedFrom) {
|
|
console.log(info(`Auto-switched to ${preflight.accountId}`));
|
|
console.log(` Reason: ${preflight.reason}`);
|
|
if (preflight.quotaPercent !== undefined && preflight.quotaPercent !== null) {
|
|
console.log(` New account quota: ${preflight.quotaPercent.toFixed(1)}%`);
|
|
} else {
|
|
console.log(` New account quota: N/A (fetch unavailable)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Error patterns indicating provider failure */
|
|
export const PROVIDER_ERROR_PATTERNS = [
|
|
/Error:\s*4[012][0-9]/i,
|
|
/Error:\s*5[0-9]{2}/i,
|
|
/overloaded/i,
|
|
/quota.*exceeded/i,
|
|
/ECONNREFUSED/i,
|
|
/rate.?limit/i,
|
|
];
|
|
|
|
/** Detect which composite tier failed from stderr output */
|
|
export function detectFailedTier(
|
|
stderr: string,
|
|
tiers: { opus: CompositeTierConfig; sonnet: CompositeTierConfig; haiku: CompositeTierConfig }
|
|
): 'opus' | 'sonnet' | 'haiku' | null {
|
|
for (const tier of ['opus', 'sonnet', 'haiku'] as const) {
|
|
if (stderr.includes(tiers[tier].model)) return tier;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Check if Claude exit indicates provider error (vs normal user exit) */
|
|
export function isProviderError(exitCode: number, stderr: string): boolean {
|
|
if (exitCode === 0) return false;
|
|
return PROVIDER_ERROR_PATTERNS.some((p) => p.test(stderr));
|
|
}
|