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