@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
289 lines • 12.1 kB
JavaScript
/**
* JAF ADK Layer - LLM Error Handling
*
* Comprehensive error handling for LLM API failures following JAF patterns
*/
import { createAdkError } from '../types.js';
// ========== Error Classifications ==========
export const LLM_ERROR_TYPES = {
// Authentication errors
INVALID_API_KEY: 'INVALID_API_KEY',
UNAUTHORIZED: 'UNAUTHORIZED',
QUOTA_EXCEEDED: 'QUOTA_EXCEEDED',
// Request errors
INVALID_REQUEST: 'INVALID_REQUEST',
MODEL_NOT_FOUND: 'MODEL_NOT_FOUND',
UNSUPPORTED_MODEL: 'UNSUPPORTED_MODEL',
INVALID_PARAMETERS: 'INVALID_PARAMETERS',
// Rate limiting
RATE_LIMITED: 'RATE_LIMITED',
CONCURRENT_LIMIT: 'CONCURRENT_LIMIT',
// Service errors
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
TIMEOUT: 'TIMEOUT',
NETWORK_ERROR: 'NETWORK_ERROR',
// Content errors
CONTENT_FILTERED: 'CONTENT_FILTERED',
CONTENT_TOO_LONG: 'CONTENT_TOO_LONG',
// Function calling errors
FUNCTION_CALL_ERROR: 'FUNCTION_CALL_ERROR',
FUNCTION_NOT_FOUND: 'FUNCTION_NOT_FOUND',
// Unknown errors
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
};
export const RETRYABLE_ERRORS = new Set([
LLM_ERROR_TYPES.RATE_LIMITED,
LLM_ERROR_TYPES.SERVICE_UNAVAILABLE,
LLM_ERROR_TYPES.TIMEOUT,
LLM_ERROR_TYPES.NETWORK_ERROR,
LLM_ERROR_TYPES.CONCURRENT_LIMIT
]);
// ========== Error Factory Functions ==========
export const createLLMError = (message, code, context) => ({
...createAdkError(message, code, context),
name: 'LLMError',
provider: context?.provider,
model: context?.model,
retryable: RETRYABLE_ERRORS.has(code),
statusCode: context?.statusCode,
responseBody: context?.responseBody
});
export const createLLMTimeoutError = (provider, model, timeout) => createLLMError(`LLM request timed out after ${timeout}ms`, LLM_ERROR_TYPES.TIMEOUT, { provider, model });
export const createLLMRateLimitError = (provider, model, resetTime) => createLLMError(`Rate limit exceeded for ${provider}/${model}${resetTime ? `. Resets in ${resetTime}ms` : ''}`, LLM_ERROR_TYPES.RATE_LIMITED, { provider, model });
export const createLLMQuotaError = (provider, model) => createLLMError(`Quota exceeded for ${provider}/${model}`, LLM_ERROR_TYPES.QUOTA_EXCEEDED, { provider, model });
export const createLLMContentFilterError = (provider, model) => createLLMError(`Content filtered by ${provider}/${model}`, LLM_ERROR_TYPES.CONTENT_FILTERED, { provider, model });
// ========== Error Classification ==========
export const classifyLLMError = (error, provider, model) => {
const message = error.message.toLowerCase();
// Authentication errors
if (message.includes('api key') || message.includes('unauthorized') || message.includes('invalid key')) {
return createLLMError(error.message, LLM_ERROR_TYPES.INVALID_API_KEY, { provider, model });
}
if (message.includes('quota') || message.includes('billing')) {
return createLLMError(error.message, LLM_ERROR_TYPES.QUOTA_EXCEEDED, { provider, model });
}
// Rate limiting
if (message.includes('rate limit') || message.includes('too many requests')) {
return createLLMError(error.message, LLM_ERROR_TYPES.RATE_LIMITED, { provider, model });
}
// Model errors
if (message.includes('model') && (message.includes('not found') || message.includes('not available'))) {
return createLLMError(error.message, LLM_ERROR_TYPES.MODEL_NOT_FOUND, { provider, model });
}
// Timeout errors
if (message.includes('timeout') || message.includes('timed out')) {
return createLLMError(error.message, LLM_ERROR_TYPES.TIMEOUT, { provider, model });
}
// Network errors
if (message.includes('network') || message.includes('connection') || message.includes('fetch')) {
return createLLMError(error.message, LLM_ERROR_TYPES.NETWORK_ERROR, { provider, model });
}
// Service errors
if (message.includes('service unavailable') || message.includes('server error') || message.includes('503')) {
return createLLMError(error.message, LLM_ERROR_TYPES.SERVICE_UNAVAILABLE, { provider, model });
}
// Content filtering
if (message.includes('content') && (message.includes('filter') || message.includes('policy'))) {
return createLLMError(error.message, LLM_ERROR_TYPES.CONTENT_FILTERED, { provider, model });
}
// Content too long
if (message.includes('too long') || message.includes('token limit') || message.includes('context length')) {
return createLLMError(error.message, LLM_ERROR_TYPES.CONTENT_TOO_LONG, { provider, model });
}
// Function calling errors
if (message.includes('function') || message.includes('tool')) {
return createLLMError(error.message, LLM_ERROR_TYPES.FUNCTION_CALL_ERROR, { provider, model });
}
// Default to unknown error
return createLLMError(error.message, LLM_ERROR_TYPES.UNKNOWN_ERROR, { provider, model });
};
// ========== Retry Logic ==========
export const DEFAULT_RETRY_CONFIG = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
retryableErrors: [
LLM_ERROR_TYPES.RATE_LIMITED,
LLM_ERROR_TYPES.SERVICE_UNAVAILABLE,
LLM_ERROR_TYPES.TIMEOUT,
LLM_ERROR_TYPES.NETWORK_ERROR,
LLM_ERROR_TYPES.CONCURRENT_LIMIT
]
};
export const shouldRetryError = (error, config = DEFAULT_RETRY_CONFIG) => {
return error.retryable === true && config.retryableErrors.includes(error.code);
};
export const calculateRetryDelay = (attempt, config = DEFAULT_RETRY_CONFIG, jitter = true) => {
const exponentialDelay = config.baseDelay * Math.pow(2, attempt);
const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
if (jitter) {
// Add random jitter to prevent thundering herd
return cappedDelay * (0.5 + Math.random() * 0.5);
}
return cappedDelay;
};
// ========== Retry Wrapper ==========
export const withLLMRetry = (fn, config = {}, provider = 'unknown', model = 'unknown') => {
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
return async (...args) => {
let lastError;
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
try {
return await fn(...args);
}
catch (error) {
lastError = error instanceof Error
? classifyLLMError(error, provider, model)
: createLLMError(String(error), LLM_ERROR_TYPES.UNKNOWN_ERROR, { provider, model });
if (attempt === retryConfig.maxRetries || !shouldRetryError(lastError, retryConfig)) {
throw lastError;
}
const delay = calculateRetryDelay(attempt, retryConfig);
console.warn(`[ADK:LLM] Attempt ${attempt + 1} failed, retrying in ${delay}ms:`, lastError.message);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};
};
// ========== Timeout Wrapper ==========
export const withLLMTimeout = (fn, timeoutMs, provider = 'unknown', model = 'unknown') => {
return async (...args) => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(createLLMTimeoutError(provider, model, timeoutMs));
}, timeoutMs);
});
return Promise.race([
fn(...args),
timeoutPromise
]);
};
};
export const createCircuitBreaker = (fn, config, provider = 'unknown', model = 'unknown') => {
const state = {
failures: 0,
lastFailureTime: 0,
state: 'closed'
};
return async (...args) => {
const now = Date.now();
// Check if we should reset from open to half-open
if (state.state === 'open' && now - state.lastFailureTime >= config.resetTimeout) {
state.state = 'half-open';
console.log(`[ADK:LLM] Circuit breaker ${provider}/${model} moving to half-open`);
}
// Reject immediately if circuit is open
if (state.state === 'open') {
throw createLLMError(`Circuit breaker open for ${provider}/${model}`, LLM_ERROR_TYPES.SERVICE_UNAVAILABLE, { provider, model });
}
try {
const result = await fn(...args);
// Success - reset circuit breaker
if (state.state === 'half-open') {
state.state = 'closed';
state.failures = 0;
console.log(`[ADK:LLM] Circuit breaker ${provider}/${model} reset to closed`);
}
return result;
}
catch (error) {
state.failures++;
state.lastFailureTime = now;
// Open circuit if threshold exceeded
if (state.failures >= config.failureThreshold) {
state.state = 'open';
console.warn(`[ADK:LLM] Circuit breaker ${provider}/${model} opened after ${state.failures} failures`);
}
throw error;
}
};
};
// ========== Error Recovery Strategies ==========
export const createFallbackStrategy = (primaryFn, fallbackFn, shouldFallback = () => true) => {
return async (...args) => {
try {
return await primaryFn(...args);
}
catch (error) {
const llmError = error instanceof Error
? classifyLLMError(error, 'unknown', 'unknown')
: createLLMError(String(error), LLM_ERROR_TYPES.UNKNOWN_ERROR);
if (shouldFallback(llmError)) {
console.warn(`[ADK:LLM] Primary function failed, using fallback:`, llmError.message);
return await fallbackFn(...args);
}
throw llmError;
}
};
};
export const createLLMErrorMonitor = () => {
const metrics = {
totalErrors: 0,
errorsByType: {},
errorsByProvider: {},
errorsByModel: {}
};
const recordError = (error) => {
metrics.totalErrors++;
metrics.errorsByType[error.code] = (metrics.errorsByType[error.code] || 0) + 1;
if (error.provider) {
metrics.errorsByProvider[error.provider] = (metrics.errorsByProvider[error.provider] || 0) + 1;
}
if (error.model) {
metrics.errorsByModel[error.model] = (metrics.errorsByModel[error.model] || 0) + 1;
}
metrics.lastError = error;
metrics.lastErrorTime = Date.now();
};
const getMetrics = () => ({ ...metrics });
const resetMetrics = () => {
metrics.totalErrors = 0;
metrics.errorsByType = {};
metrics.errorsByProvider = {};
metrics.errorsByModel = {};
delete metrics.lastError;
delete metrics.lastErrorTime;
};
return {
recordError,
getMetrics,
resetMetrics
};
};
export const createLLMErrorLogger = () => ({
logError: (error, context) => {
console.error(`[ADK:LLM:ERROR] ${error.code}: ${error.message}`, {
provider: error.provider,
model: error.model,
retryable: error.retryable,
statusCode: error.statusCode,
context
});
},
logRetry: (error, attempt, delay) => {
console.warn(`[ADK:LLM:RETRY] Attempt ${attempt} failed: ${error.message}. Retrying in ${delay}ms`, {
provider: error.provider,
model: error.model,
code: error.code
});
},
logRecovery: (error, recoveryMethod) => {
console.info(`[ADK:LLM:RECOVERY] Recovered from error using ${recoveryMethod}: ${error.message}`, {
provider: error.provider,
model: error.model,
code: error.code
});
}
});
// ========== Default Export ==========
export const defaultLLMErrorHandler = {
classify: classifyLLMError,
retry: withLLMRetry,
timeout: withLLMTimeout,
fallback: createFallbackStrategy,
circuitBreaker: createCircuitBreaker,
monitor: createLLMErrorMonitor(),
logger: createLLMErrorLogger()
};
//# sourceMappingURL=error-handler.js.map