mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 10:03:08 +00:00
320 lines
9.7 KiB
TypeScript
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);
|
|
}
|