File size: 9,943 Bytes
1dbc34b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | /**
* Error handling utilities for standardized error classification
*
* Provides utilities for:
* - Detecting abort/cancellation errors
* - Detecting authentication errors
* - Detecting rate limit and quota exhaustion errors
* - Classifying errors by type
* - Generating user-friendly error messages
*/
import type { ErrorType, ErrorInfo } from '@automaker/types';
/**
* Check if an error is an abort/cancellation error
*
* @param error - The error to check
* @returns True if the error is an abort error
*/
export function isAbortError(error: unknown): boolean {
return error instanceof Error && (error.name === 'AbortError' || error.message.includes('abort'));
}
/**
* Check if an error is a user-initiated cancellation
*
* @param errorMessage - The error message to check
* @returns True if the error is a user-initiated cancellation
*/
export function isCancellationError(errorMessage: string): boolean {
const lowerMessage = errorMessage.toLowerCase();
return (
lowerMessage.includes('cancelled') ||
lowerMessage.includes('canceled') ||
lowerMessage.includes('stopped') ||
lowerMessage.includes('aborted')
);
}
/**
* Check if an error is an authentication/API key error
*
* @param errorMessage - The error message to check
* @returns True if the error is authentication-related
*/
export function isAuthenticationError(errorMessage: string): boolean {
return (
errorMessage.includes('Authentication failed') ||
errorMessage.includes('Invalid API key') ||
errorMessage.includes('authentication_failed') ||
errorMessage.includes('Fix external API key')
);
}
/**
* Check if an error is a rate limit error (429 Too Many Requests)
*
* @param error - The error to check
* @returns True if the error is a rate limit error
*/
export function isRateLimitError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error || '');
return message.includes('429') || message.includes('rate_limit');
}
/**
* Check if an error indicates quota/usage exhaustion
* This includes session limits, weekly limits, credit/billing issues, and overloaded errors
*
* @param error - The error to check
* @returns True if the error indicates quota exhaustion
*/
export function isQuotaExhaustedError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error || '');
const lowerMessage = message.toLowerCase();
// Check for overloaded/capacity errors
if (
lowerMessage.includes('overloaded') ||
lowerMessage.includes('overloaded_error') ||
lowerMessage.includes('capacity')
) {
return true;
}
// Check for usage/quota limit patterns
if (
lowerMessage.includes('limit reached') ||
lowerMessage.includes('usage limit') ||
lowerMessage.includes('quota exceeded') ||
lowerMessage.includes('quota_exceeded') ||
lowerMessage.includes('session limit') ||
lowerMessage.includes('weekly limit') ||
lowerMessage.includes('monthly limit')
) {
return true;
}
// Check for billing/credit issues
if (
lowerMessage.includes('credit balance') ||
lowerMessage.includes('insufficient credits') ||
lowerMessage.includes('insufficient balance') ||
lowerMessage.includes('no credits') ||
lowerMessage.includes('out of credits') ||
lowerMessage.includes('billing') ||
lowerMessage.includes('payment required')
) {
return true;
}
// Check for upgrade prompts (often indicates limit reached)
if (lowerMessage.includes('/upgrade') || lowerMessage.includes('extra-usage')) {
return true;
}
return false;
}
/**
* Check if an error indicates a model-not-found or model access issue
*
* @param error - The error to check
* @returns True if the error indicates the model doesn't exist or user lacks access
*/
export function isModelNotFoundError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error || '');
const lowerMessage = message.toLowerCase();
return (
lowerMessage.includes('does not exist or you do not have access') ||
lowerMessage.includes('model_not_found') ||
lowerMessage.includes('invalid_model') ||
(lowerMessage.includes('model') &&
(lowerMessage.includes('does not exist') || lowerMessage.includes('not found')))
);
}
/**
* Check if an error indicates a stream disconnection
*
* @param error - The error to check
* @returns True if the error indicates the stream was disconnected
*/
export function isStreamDisconnectedError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error || '');
const lowerMessage = message.toLowerCase();
return (
lowerMessage.includes('stream disconnected') ||
lowerMessage.includes('stream ended') ||
lowerMessage.includes('connection reset') ||
lowerMessage.includes('socket hang up') ||
lowerMessage.includes('econnreset')
);
}
/**
* Extract retry-after duration from rate limit error
*
* @param error - The error to extract retry-after from
* @returns Number of seconds to wait, or undefined if not found
*/
export function extractRetryAfter(error: unknown): number | undefined {
const message = error instanceof Error ? error.message : String(error || '');
// Try to extract from Retry-After header format
const retryMatch = message.match(/retry[_-]?after[:\s]+(\d+)/i);
if (retryMatch) {
return parseInt(retryMatch[1], 10);
}
// Try to extract from error message patterns
const waitMatch = message.match(/wait[:\s]+(\d+)\s*(?:second|sec|s)/i);
if (waitMatch) {
return parseInt(waitMatch[1], 10);
}
return undefined;
}
/**
* Classify an error into a specific type
*
* @param error - The error to classify
* @returns Classified error information
*/
export function classifyError(error: unknown): ErrorInfo {
const message = error instanceof Error ? error.message : String(error || 'Unknown error');
const isAbort = isAbortError(error);
const isAuth = isAuthenticationError(message);
const isCancellation = isCancellationError(message);
const isRateLimit = isRateLimitError(error);
const isQuotaExhausted = isQuotaExhaustedError(error);
const isModelNotFound = isModelNotFoundError(error);
const isStreamDisconnected = isStreamDisconnectedError(error);
const retryAfter = isRateLimit ? (extractRetryAfter(error) ?? 60) : undefined;
let type: ErrorType;
if (isAuth) {
type = 'authentication';
} else if (isModelNotFound) {
type = 'model_not_found';
} else if (isStreamDisconnected) {
type = 'stream_disconnected';
} else if (isQuotaExhausted) {
// Quota exhaustion takes priority over rate limit since it's more specific
type = 'quota_exhausted';
} else if (isRateLimit) {
type = 'rate_limit';
} else if (isAbort) {
type = 'abort';
} else if (isCancellation) {
type = 'cancellation';
} else if (error instanceof Error) {
type = 'execution';
} else {
type = 'unknown';
}
return {
type,
message,
isAbort,
isAuth,
isCancellation,
isRateLimit,
isQuotaExhausted,
isModelNotFound,
isStreamDisconnected,
retryAfter,
originalError: error,
};
}
/**
* Get a user-friendly error message
*
* @param error - The error to convert
* @returns User-friendly error message
*/
export function getUserFriendlyErrorMessage(error: unknown): string {
const info = classifyError(error);
if (info.isAbort) {
return 'Operation was cancelled';
}
if (info.isAuth) {
return 'Authentication failed. Please check your API key.';
}
if (info.isModelNotFound) {
return `Model not available: ${info.message}\n\nSome models require specific subscription plans or authentication methods. Try authenticating with 'codex login' or switch to a different model.`;
}
if (info.isStreamDisconnected) {
return `Connection interrupted: ${info.message}\n\nThe stream was disconnected before the response could complete. This may be caused by network issues, model access restrictions, or server timeouts. Try again or switch to a different model.`;
}
if (info.isQuotaExhausted) {
return 'Usage limit reached. Auto Mode has been paused. Please wait for your quota to reset or upgrade your plan.';
}
if (info.isRateLimit) {
const retryMsg = info.retryAfter
? ` Please wait ${info.retryAfter} seconds before retrying.`
: ' Please reduce concurrency or wait before retrying.';
return `Rate limit exceeded (429).${retryMsg}`;
}
return info.message;
}
/**
* Extract error message from an unknown error value
*
* Simple utility for getting a string error message from any error type.
* Returns the error's message property if it's an Error, otherwise
* converts to string. Used throughout the codebase for consistent
* error message extraction.
*
* @param error - The error value (Error object, string, or unknown)
* @returns Error message string
*
* @example
* ```typescript
* try {
* throw new Error("Something went wrong");
* } catch (error) {
* const message = getErrorMessage(error); // "Something went wrong"
* }
* ```
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : 'Unknown error';
}
/**
* Log an error with a context message to stderr.
*
* Convenience utility for consistent error logging throughout the codebase.
* Outputs a formatted error line to stderr with an ❌ prefix and the context.
*
* @param error - The error value to log
* @param context - Descriptive context message indicating where/why the error occurred
*
* @example
* ```typescript
* try {
* await someOperation();
* } catch (error) {
* logError(error, 'Failed to perform some operation');
* }
* ```
*/
export function logError(error: unknown, context: string): void {
console.error(`❌ ${context}:`, error);
}
|