/** * Provider error translation layer. * Normalizes provider-specific errors (AWS, Anthropic, HTTP) into * OpenAI-compatible error format for consistent API responses. * * @param {Error|object} error - The original error object * @param {string} providerType - Provider type: 'bedrock', 'aws', 'anthropic', 'openai-compatible', 'generic' * @returns {{ error: { message: string, type: string, param: null, code: string }, status: number }} */ export function translateProviderError(error, providerType) { if (!error) { return makeError('An unexpected error occurred', 'api_error', 'internal_error', 502); } // Network errors are handled the same regardless of provider if (isNetworkError(error)) { return makeError( 'Unable to connect to the provider service. Please check your connection and try again.', 'api_error', 'connection_error', 502 ); } const type = (providerType || '').toLowerCase(); if (type === 'bedrock' || type === 'aws') { return translateAwsError(error); } if (type === 'anthropic') { return translateAnthropicError(error); } if (type === 'openai-compatible' || type === 'generic') { return translateHttpError(error); } // Unknown provider type — fall back to generic error return makeError( sanitizeMessage(error.message) || 'An unexpected error occurred', 'api_error', 'internal_error', 502 ); } // --------------------------------------------------------------------------- // Provider-specific translators // --------------------------------------------------------------------------- /** * Translate an AWS SDK error into OpenAI format. * AWS SDK errors expose a `name` property (e.g. ThrottlingException). * * @param {Error} error - AWS SDK error * @returns {{ error: object, status: number }} */ function translateAwsError(error) { const name = error.name || ''; const message = sanitizeMessage(error.message) || 'A Bedrock service error occurred'; switch (name) { case 'ThrottlingException': return makeError(message, 'rate_limit_error', 'rate_limit_exceeded', 429); case 'ValidationException': return makeError(message, 'invalid_request_error', 'invalid_request_error', 400); case 'AccessDeniedException': return makeError(message, 'authentication_error', 'access_denied', 403); case 'ResourceNotFoundException': return makeError(message, 'invalid_request_error', 'model_not_found', 404); default: return makeError(message, 'api_error', 'internal_error', 502); } } /** * Translate an Anthropic API error into OpenAI format. * Anthropic errors have a `type` property in the response body * (e.g. overloaded_error, invalid_request_error). * * @param {Error|object} error - Anthropic error (may be raw parsed body) * @returns {{ error: object, status: number }} */ function translateAnthropicError(error) { // Anthropic error shape: { type, message } on the error body itself, // or { error: { type, message } } when wrapped in a response envelope const errorType = (error.type) || (error.error && error.error.type) || ''; const message = sanitizeMessage( error.message || (error.error && error.error.message) || 'An Anthropic service error occurred' ); switch (errorType) { case 'overloaded_error': return makeError(message, 'api_error', 'server_overloaded', 503); case 'invalid_request_error': return makeError(message, 'invalid_request_error', 'invalid_request_error', 400); case 'authentication_error': return makeError(message, 'authentication_error', 'invalid_api_key', 401); case 'not_found_error': return makeError(message, 'invalid_request_error', 'model_not_found', 404); default: return makeError(message, 'api_error', 'internal_error', 502); } } /** * Translate an HTTP/generic error into OpenAI format using status code. * * @param {Error|object} error - Error with a status/statusCode property * @returns {{ error: object, status: number }} */ function translateHttpError(error) { const statusCode = error?.status || error?.statusCode || error?.response?.status || 0; const message = sanitizeMessage(error?.message) || 'An API error occurred'; switch (statusCode) { case 401: return makeError(message, 'authentication_error', 'invalid_api_key', 401); case 404: return makeError(message, 'invalid_request_error', 'model_not_found', 404); case 429: return makeError(message, 'rate_limit_error', 'rate_limit_exceeded', 429); default: return makeError(message, 'api_error', 'internal_error', 502); } } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Build an OpenAI-format error response. * * @param {string} message - User-facing error message * @param {string} type - OpenAI error type * @param {string} code - Machine-readable error code * @param {number} status - HTTP status code * @returns {{ error: { message: string, type: string, param: null, code: string }, status: number }} */ function makeError(message, type, code, status) { return { error: { message, type, param: null, code }, status }; } /** * Check whether an error is a network-level failure (fetch failed, connection refused, etc.). * * @param {Error} error * @returns {boolean} */ function isNetworkError(error) { if (!error || !error.message) return false; const msg = error.message.toLowerCase(); return ( msg.includes('fetch failed') || msg.includes('network error') || msg.includes('connection refused') || msg.includes('connect econnrefused') || msg.includes('enotfound') || msg.includes('econnreset') || msg.includes('econnaborted') || msg.includes('socket hang up') || msg.includes('request timeout') || msg.includes('eai_again') ); } /** * Sanitize an error message to remove credentials, keys, URLs, and other * sensitive information before returning to the client. * * @param {string} [message] * @returns {string} */ function sanitizeMessage(message) { if (typeof message !== 'string' || !message) return ''; return message // Redact AWS access key IDs (AKIA...) .replace(/AKIA[0-9A-Z]{16}/g, '[REDACTED]') // Redact potential secret keys / tokens (base64-like sequences of 40+ chars) .replace(/[A-Za-z0-9+/]{40,}={0,2}/g, '[REDACTED]') // Redact URLs .replace(/https?:\/\/[^\s]+/g, '[REDACTED]') // Redact bearer tokens .replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]') // Redact x-api-key values .replace(/x-api-key:\s*\S+/gi, 'x-api-key: [REDACTED]'); }