errors.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * Provider error translation layer.
  3. * Normalizes provider-specific errors (AWS, Anthropic, HTTP) into
  4. * OpenAI-compatible error format for consistent API responses.
  5. *
  6. * @param {Error|object} error - The original error object
  7. * @param {string} providerType - Provider type: 'bedrock', 'aws', 'anthropic', 'openai-compatible', 'generic'
  8. * @returns {{ error: { message: string, type: string, param: null, code: string }, status: number }}
  9. */
  10. export function translateProviderError(error, providerType) {
  11. if (!error) {
  12. return makeError('An unexpected error occurred', 'api_error', 'internal_error', 502);
  13. }
  14. // Network errors are handled the same regardless of provider
  15. if (isNetworkError(error)) {
  16. return makeError(
  17. 'Unable to connect to the provider service. Please check your connection and try again.',
  18. 'api_error',
  19. 'connection_error',
  20. 502
  21. );
  22. }
  23. const type = (providerType || '').toLowerCase();
  24. if (type === 'bedrock' || type === 'aws') {
  25. return translateAwsError(error);
  26. }
  27. if (type === 'anthropic') {
  28. return translateAnthropicError(error);
  29. }
  30. if (type === 'openai-compatible' || type === 'generic') {
  31. return translateHttpError(error);
  32. }
  33. // Unknown provider type — fall back to generic error
  34. return makeError(
  35. sanitizeMessage(error.message) || 'An unexpected error occurred',
  36. 'api_error',
  37. 'internal_error',
  38. 502
  39. );
  40. }
  41. // ---------------------------------------------------------------------------
  42. // Provider-specific translators
  43. // ---------------------------------------------------------------------------
  44. /**
  45. * Translate an AWS SDK error into OpenAI format.
  46. * AWS SDK errors expose a `name` property (e.g. ThrottlingException).
  47. *
  48. * @param {Error} error - AWS SDK error
  49. * @returns {{ error: object, status: number }}
  50. */
  51. function translateAwsError(error) {
  52. const name = error.name || '';
  53. const message = sanitizeMessage(error.message) || 'A Bedrock service error occurred';
  54. switch (name) {
  55. case 'ThrottlingException':
  56. return makeError(message, 'rate_limit_error', 'rate_limit_exceeded', 429);
  57. case 'ValidationException':
  58. return makeError(message, 'invalid_request_error', 'invalid_request_error', 400);
  59. case 'AccessDeniedException':
  60. return makeError(message, 'authentication_error', 'access_denied', 403);
  61. case 'ResourceNotFoundException':
  62. return makeError(message, 'invalid_request_error', 'model_not_found', 404);
  63. default:
  64. return makeError(message, 'api_error', 'internal_error', 502);
  65. }
  66. }
  67. /**
  68. * Translate an Anthropic API error into OpenAI format.
  69. * Anthropic errors have a `type` property in the response body
  70. * (e.g. overloaded_error, invalid_request_error).
  71. *
  72. * @param {Error|object} error - Anthropic error (may be raw parsed body)
  73. * @returns {{ error: object, status: number }}
  74. */
  75. function translateAnthropicError(error) {
  76. // Anthropic error shape: { type, message } on the error body itself,
  77. // or { error: { type, message } } when wrapped in a response envelope
  78. const errorType =
  79. (error.type) ||
  80. (error.error && error.error.type) ||
  81. '';
  82. const message = sanitizeMessage(
  83. error.message ||
  84. (error.error && error.error.message) ||
  85. 'An Anthropic service error occurred'
  86. );
  87. switch (errorType) {
  88. case 'overloaded_error':
  89. return makeError(message, 'api_error', 'server_overloaded', 503);
  90. case 'invalid_request_error':
  91. return makeError(message, 'invalid_request_error', 'invalid_request_error', 400);
  92. case 'authentication_error':
  93. return makeError(message, 'authentication_error', 'invalid_api_key', 401);
  94. case 'not_found_error':
  95. return makeError(message, 'invalid_request_error', 'model_not_found', 404);
  96. default:
  97. return makeError(message, 'api_error', 'internal_error', 502);
  98. }
  99. }
  100. /**
  101. * Translate an HTTP/generic error into OpenAI format using status code.
  102. *
  103. * @param {Error|object} error - Error with a status/statusCode property
  104. * @returns {{ error: object, status: number }}
  105. */
  106. function translateHttpError(error) {
  107. const statusCode = error?.status || error?.statusCode || error?.response?.status || 0;
  108. const message = sanitizeMessage(error?.message) || 'An API error occurred';
  109. switch (statusCode) {
  110. case 401:
  111. return makeError(message, 'authentication_error', 'invalid_api_key', 401);
  112. case 404:
  113. return makeError(message, 'invalid_request_error', 'model_not_found', 404);
  114. case 429:
  115. return makeError(message, 'rate_limit_error', 'rate_limit_exceeded', 429);
  116. default:
  117. return makeError(message, 'api_error', 'internal_error', 502);
  118. }
  119. }
  120. // ---------------------------------------------------------------------------
  121. // Helpers
  122. // ---------------------------------------------------------------------------
  123. /**
  124. * Build an OpenAI-format error response.
  125. *
  126. * @param {string} message - User-facing error message
  127. * @param {string} type - OpenAI error type
  128. * @param {string} code - Machine-readable error code
  129. * @param {number} status - HTTP status code
  130. * @returns {{ error: { message: string, type: string, param: null, code: string }, status: number }}
  131. */
  132. function makeError(message, type, code, status) {
  133. return {
  134. error: { message, type, param: null, code },
  135. status
  136. };
  137. }
  138. /**
  139. * Check whether an error is a network-level failure (fetch failed, connection refused, etc.).
  140. *
  141. * @param {Error} error
  142. * @returns {boolean}
  143. */
  144. function isNetworkError(error) {
  145. if (!error || !error.message) return false;
  146. const msg = error.message.toLowerCase();
  147. return (
  148. msg.includes('fetch failed') ||
  149. msg.includes('network error') ||
  150. msg.includes('connection refused') ||
  151. msg.includes('connect econnrefused') ||
  152. msg.includes('enotfound') ||
  153. msg.includes('econnreset') ||
  154. msg.includes('econnaborted') ||
  155. msg.includes('socket hang up') ||
  156. msg.includes('request timeout') ||
  157. msg.includes('eai_again')
  158. );
  159. }
  160. /**
  161. * Sanitize an error message to remove credentials, keys, URLs, and other
  162. * sensitive information before returning to the client.
  163. *
  164. * @param {string} [message]
  165. * @returns {string}
  166. */
  167. function sanitizeMessage(message) {
  168. if (typeof message !== 'string' || !message) return '';
  169. return message
  170. // Redact AWS access key IDs (AKIA...)
  171. .replace(/AKIA[0-9A-Z]{16}/g, '[REDACTED]')
  172. // Redact potential secret keys / tokens (base64-like sequences of 40+ chars)
  173. .replace(/[A-Za-z0-9+/]{40,}={0,2}/g, '[REDACTED]')
  174. // Redact URLs
  175. .replace(/https?:\/\/[^\s]+/g, '[REDACTED]')
  176. // Redact bearer tokens
  177. .replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
  178. // Redact x-api-key values
  179. .replace(/x-api-key:\s*\S+/gi, 'x-api-key: [REDACTED]');
  180. }