Files
automaker/libs/utils/src/error-handler.ts

320 lines
9.7 KiB
TypeScript

/**
* 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);
}