mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-04-20 03:02:51 -04:00
refactor: enhance error handling and normalization across AI engines
This update introduces a centralized error handling mechanism for various AI engines, improving the consistency and clarity of error messages. The new `normalizeEngineError` function standardizes error responses, allowing for better user feedback and recovery suggestions. Additionally, specific error classes for insufficient credits, rate limits, and service availability have been implemented, along with user-friendly formatting for error messages. This refactor aims to enhance the overall user experience when interacting with the AI services.
This commit is contained in:
205
src/utils/engineErrorHandler.ts
Normal file
205
src/utils/engineErrorHandler.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import axios from 'axios';
|
||||
import {
|
||||
AuthenticationError,
|
||||
InsufficientCreditsError,
|
||||
ModelNotFoundError,
|
||||
RateLimitError,
|
||||
ServiceUnavailableError
|
||||
} from './errors';
|
||||
|
||||
/**
|
||||
* Extracts HTTP status code from various error types
|
||||
*/
|
||||
function getStatusCode(error: unknown): number | null {
|
||||
// Direct status property (common in API SDKs)
|
||||
if (typeof (error as any)?.status === 'number') {
|
||||
return (error as any).status;
|
||||
}
|
||||
|
||||
// Axios-style errors
|
||||
if (axios.isAxiosError(error)) {
|
||||
return error.response?.status ?? null;
|
||||
}
|
||||
|
||||
// Response object with status
|
||||
if (typeof (error as any)?.response?.status === 'number') {
|
||||
return (error as any).response.status;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts retry-after value from error headers (for rate limiting)
|
||||
*/
|
||||
function getRetryAfter(error: unknown): number | undefined {
|
||||
const headers = (error as any)?.response?.headers;
|
||||
if (headers) {
|
||||
const retryAfter = headers['retry-after'] || headers['Retry-After'];
|
||||
if (retryAfter) {
|
||||
const seconds = parseInt(retryAfter, 10);
|
||||
if (!isNaN(seconds)) {
|
||||
return seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the error message from various error structures
|
||||
*/
|
||||
function extractErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
// API error response structures
|
||||
const apiError = (error as any)?.response?.data?.error;
|
||||
if (apiError) {
|
||||
if (typeof apiError === 'string') {
|
||||
return apiError;
|
||||
}
|
||||
if (apiError.message) {
|
||||
return apiError.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Direct error data
|
||||
const errorData = (error as any)?.error;
|
||||
if (errorData) {
|
||||
if (typeof errorData === 'string') {
|
||||
return errorData;
|
||||
}
|
||||
if (errorData.message) {
|
||||
return errorData.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
return 'An unknown error occurred';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the error message indicates a model not found error
|
||||
*/
|
||||
function isModelNotFoundMessage(message: string): boolean {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
return (
|
||||
(lowerMessage.includes('model') &&
|
||||
(lowerMessage.includes('not found') ||
|
||||
lowerMessage.includes('does not exist') ||
|
||||
lowerMessage.includes('invalid') ||
|
||||
lowerMessage.includes('pull'))) ||
|
||||
lowerMessage.includes('does_not_exist')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the error message indicates insufficient credits
|
||||
*/
|
||||
function isInsufficientCreditsMessage(message: string): boolean {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
return (
|
||||
lowerMessage.includes('insufficient') ||
|
||||
lowerMessage.includes('credit') ||
|
||||
lowerMessage.includes('quota') ||
|
||||
lowerMessage.includes('balance too low') ||
|
||||
lowerMessage.includes('billing') ||
|
||||
lowerMessage.includes('payment required') ||
|
||||
lowerMessage.includes('exceeded')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes raw API errors into typed error classes.
|
||||
* This provides consistent error handling across all engine implementations.
|
||||
*
|
||||
* @param error - The raw error from the API call
|
||||
* @param provider - The AI provider name (e.g., 'openai', 'anthropic')
|
||||
* @param model - The model being used
|
||||
* @returns A typed Error instance
|
||||
*/
|
||||
export function normalizeEngineError(
|
||||
error: unknown,
|
||||
provider: string,
|
||||
model: string
|
||||
): Error {
|
||||
// If it's already one of our custom errors, return as-is
|
||||
if (
|
||||
error instanceof ModelNotFoundError ||
|
||||
error instanceof AuthenticationError ||
|
||||
error instanceof InsufficientCreditsError ||
|
||||
error instanceof RateLimitError ||
|
||||
error instanceof ServiceUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const statusCode = getStatusCode(error);
|
||||
const message = extractErrorMessage(error);
|
||||
|
||||
// Handle based on HTTP status codes
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
return new AuthenticationError(provider, message);
|
||||
|
||||
case 402:
|
||||
return new InsufficientCreditsError(provider, message);
|
||||
|
||||
case 404:
|
||||
// Could be model not found or endpoint not found
|
||||
if (isModelNotFoundMessage(message)) {
|
||||
return new ModelNotFoundError(model, provider, 404);
|
||||
}
|
||||
// Return generic error for other 404s
|
||||
return error instanceof Error ? error : new Error(message);
|
||||
|
||||
case 429:
|
||||
const retryAfter = getRetryAfter(error);
|
||||
return new RateLimitError(provider, retryAfter, message);
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
return new ServiceUnavailableError(provider, statusCode, message);
|
||||
}
|
||||
|
||||
// Handle based on error message content
|
||||
if (isModelNotFoundMessage(message)) {
|
||||
return new ModelNotFoundError(model, provider, 404);
|
||||
}
|
||||
|
||||
if (isInsufficientCreditsMessage(message)) {
|
||||
return new InsufficientCreditsError(provider, message);
|
||||
}
|
||||
|
||||
// Check for rate limit patterns in message
|
||||
const lowerMessage = message.toLowerCase();
|
||||
if (
|
||||
lowerMessage.includes('rate limit') ||
|
||||
lowerMessage.includes('rate_limit') ||
|
||||
lowerMessage.includes('too many requests')
|
||||
) {
|
||||
return new RateLimitError(provider, undefined, message);
|
||||
}
|
||||
|
||||
// Check for auth patterns in message
|
||||
if (
|
||||
lowerMessage.includes('unauthorized') ||
|
||||
lowerMessage.includes('api key') ||
|
||||
lowerMessage.includes('apikey') ||
|
||||
lowerMessage.includes('authentication') ||
|
||||
lowerMessage.includes('invalid_api_key')
|
||||
) {
|
||||
return new AuthenticationError(provider, message);
|
||||
}
|
||||
|
||||
// Return original error or wrap in Error if needed
|
||||
return error instanceof Error ? error : new Error(message);
|
||||
}
|
||||
Reference in New Issue
Block a user