refactor: Implement provider-agnostic error handling architecture

Transform OpenAI-specific error handling into extensible multi-provider system
that supports OpenAI, Google AI, Anthropic, Ollama, and future providers.

## Backend Enhancements
- Add ProviderErrorAdapter pattern with provider-specific implementations
- Create ProviderErrorFactory for unified error handling across providers
- Refactor API key validation to detect and handle any provider
- Update error sanitization to use provider-specific patterns
- Add provider context to all error responses

## Frontend Enhancements
- Rename interfaces from OpenAI-specific to provider-agnostic
- Update error detection to work with any provider name
- Add provider context to error messages and guidance
- Support provider-specific error codes and classifications

## Provider Support Added
 OpenAI: sk-* keys, org/proj/req IDs, quota/rate limit patterns
 Google AI: AIza* keys, googleapis.com URLs, project patterns
 Anthropic: sk-ant-* keys, anthropic.com URLs
 Ollama: localhost URLs, connection patterns (no API keys)

## Error Message Examples
- OpenAI: 'Invalid or expired OpenAI API key. Please check your API key in settings.'
- Google: 'Invalid or expired Google API key. Please check your API key in settings.'
- Anthropic: 'Invalid or expired Anthropic API key. Please check your API key in settings.'

## Security Features
- Provider-specific sanitization patterns prevent data exposure
- Auto-detection of provider from error content
- Structured error codes for reliable classification
- Enhanced input validation and ReDoS protection

This addresses the code review feedback to make error handling truly generic
and extensible for all LLM providers, not just OpenAI, while maintaining
the same level of user experience and security for each provider.
This commit is contained in:
leex279
2025-09-12 21:47:25 +02:00
parent f2ce5f959e
commit 185b952c62
5 changed files with 541 additions and 154 deletions

View File

@@ -275,7 +275,7 @@ export function useCrawlUrl() {
}
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
const errorMessage = (error as EnhancedError)?.isProviderError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to start crawl");
@@ -455,7 +455,7 @@ export function useUploadDocument() {
}
// Use enhanced error handling for better user experience
const message = (error as EnhancedError)?.isOpenAIError
const message = (error as EnhancedError)?.isProviderError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to upload document");
showToast(message, "error");
@@ -529,7 +529,7 @@ export function useDeleteKnowledgeItem() {
}
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
const errorMessage = (error as EnhancedError)?.isProviderError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to delete item");
showToast(errorMessage, "error");
@@ -579,7 +579,7 @@ export function useUpdateKnowledgeItem() {
}
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
const errorMessage = (error as EnhancedError)?.isProviderError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to update item");
showToast(errorMessage, "error");
@@ -618,7 +618,7 @@ export function useRefreshKnowledgeItem() {
},
onError: (error) => {
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
const errorMessage = (error as EnhancedError)?.isProviderError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to refresh item");
showToast(errorMessage, "error");

View File

@@ -24,54 +24,63 @@ export async function callKnowledgeAPI<T>(
// The ETag client extracts the error message but loses the structured details
// We need to reconstruct the structured error based on the status code and message
// Use status code and message patterns to identify OpenAI errors
// More reliable than exact string matching
if (error.statusCode === 401 && error.message.includes("OpenAI API key")) {
// This is our OpenAI authentication error
// Detect provider from error message and use appropriate error structure
let provider = "LLM";
if (error.message.includes("OpenAI")) provider = "OpenAI";
else if (error.message.includes("Google")) provider = "Google";
else if (error.message.includes("Anthropic")) provider = "Anthropic";
else if (error.message.includes("Ollama")) provider = "Ollama";
if (error.statusCode === 401 && error.message.toLowerCase().includes("api key")) {
// Generic authentication error
errorData = {
status: 401,
error: error.message,
detail: {
error: "Invalid OpenAI API key",
message: "Please verify your OpenAI API key in Settings before starting a crawl.",
error: `Invalid ${provider} API key`,
message: `Please verify your ${provider} API key in Settings before starting a crawl.`,
error_type: "authentication_failed",
error_code: "OPENAI_AUTH_FAILED"
error_code: `${provider.toUpperCase()}_AUTH_FAILED`,
provider: provider.toLowerCase()
}
};
} else if (error.statusCode === 429 && error.message.includes("quota")) {
// This is our OpenAI quota error
} else if (error.statusCode === 429 && error.message.toLowerCase().includes("quota")) {
// Generic quota error
errorData = {
status: 429,
error: error.message,
detail: {
error: "OpenAI quota exhausted",
message: "Your OpenAI API key has no remaining credits. Please add credits to your account.",
error: `${provider} quota exhausted`,
message: `Your ${provider} API quota has been exceeded. Please check your billing settings.`,
error_type: "quota_exhausted",
error_code: "OPENAI_QUOTA_EXHAUSTED"
error_code: `${provider.toUpperCase()}_QUOTA_EXHAUSTED`,
provider: provider.toLowerCase()
}
};
} else if (error.statusCode === 429 && error.message.includes("rate limit")) {
// This is our rate limit error
} else if (error.statusCode === 429 && error.message.toLowerCase().includes("rate limit")) {
// Generic rate limit error
errorData = {
status: 429,
error: error.message,
detail: {
error: "OpenAI API rate limit exceeded",
message: "Too many requests to OpenAI API. Please wait a moment and try again.",
error: `${provider} API rate limit exceeded`,
message: `Too many requests to ${provider} API. Please wait a moment and try again.`,
error_type: "rate_limit",
error_code: "OPENAI_RATE_LIMIT"
error_code: `${provider.toUpperCase()}_RATE_LIMIT`,
provider: provider.toLowerCase()
}
};
} else if (error.statusCode === 502 && error.message.includes("OpenAI")) {
// This is our generic API error
} else if (error.statusCode === 502 && (error.message.toLowerCase().includes("api") || error.message.includes(provider))) {
// Generic API error
errorData = {
status: 502,
error: error.message,
detail: {
error: "OpenAI API error",
message: "OpenAI API error. Please check your API key configuration.",
error: `${provider} API error`,
message: `${provider} API error. Please check your API key configuration.`,
error_type: "api_error",
error_code: "OPENAI_API_ERROR"
error_code: `${provider.toUpperCase()}_API_ERROR`,
provider: provider.toLowerCase()
}
};
} else {

View File

@@ -8,11 +8,12 @@
* by displaying clear error messages when OpenAI API fails.
*/
export interface OpenAIErrorDetails {
export interface ProviderErrorDetails {
error: string;
message: string;
error_type: 'quota_exhausted' | 'rate_limit' | 'api_error' | 'authentication_failed' | 'timeout_error' | 'configuration_error';
error_code?: string; // Structured error code for reliable detection
provider?: string; // LLM provider (openai, google, anthropic, ollama)
tokens_used?: number;
retry_after?: number;
api_key_prefix?: string;
@@ -20,8 +21,8 @@ export interface OpenAIErrorDetails {
export interface EnhancedError extends Error {
statusCode?: number;
errorDetails?: OpenAIErrorDetails;
isOpenAIError?: boolean;
errorDetails?: ProviderErrorDetails;
isProviderError?: boolean; // Renamed from isOpenAIError for genericity
}
/**
@@ -104,10 +105,10 @@ export function parseKnowledgeBaseError(error: any): EnhancedError {
// Prioritize error.detail (where we put structured OpenAI error data)
const errorData = error.detail || error.error;
// Check if it's an OpenAI-specific error
// Check if it's a provider-specific error
if (typeof errorData === 'object' && errorData?.error_type) {
enhancedError.isOpenAIError = true;
enhancedError.errorDetails = errorData as OpenAIErrorDetails;
enhancedError.isProviderError = true;
enhancedError.errorDetails = errorData as ProviderErrorDetails;
// Override the message with the detailed error message
enhancedError.message = errorData.message || errorData.error || enhancedError.message;
@@ -122,25 +123,26 @@ export function parseKnowledgeBaseError(error: any): EnhancedError {
* Get user-friendly error message for display in UI
*/
export function getDisplayErrorMessage(error: EnhancedError): string {
if (error.isOpenAIError && error.errorDetails) {
if (error.isProviderError && error.errorDetails) {
const provider = error.errorDetails.provider ? error.errorDetails.provider.charAt(0).toUpperCase() + error.errorDetails.provider.slice(1) : 'LLM';
switch (error.errorDetails.error_type) {
case 'quota_exhausted':
return `OpenAI API quota exhausted. Please add credits to your OpenAI account or check your billing settings.`;
return `${provider} API quota exhausted. Please add credits to your ${provider} account or check your billing settings.`;
case 'rate_limit':
return `OpenAI API rate limit exceeded. Please wait a moment and try again.`;
return `${provider} API rate limit exceeded. Please wait a moment and try again.`;
case 'authentication_failed':
return `Invalid or expired OpenAI API key. Please check your API key in settings.`;
return `Invalid or expired ${provider} API key. Please check your API key in settings.`;
case 'api_error':
return `OpenAI API error: ${error.errorDetails.message}. Please check your API key configuration.`;
return `${provider} API error: ${error.errorDetails.message}. Please check your API key configuration.`;
case 'timeout_error':
return `Request timed out. Please try again or check your network connection.`;
case 'configuration_error':
return `OpenAI API configuration error. Please check your API key settings.`;
return `${provider} API configuration error. Please check your API key settings.`;
default:
return error.errorDetails.message || error.message;
@@ -170,7 +172,7 @@ export function getDisplayErrorMessage(error: EnhancedError): string {
* Get error severity level for UI styling
*/
export function getErrorSeverity(error: EnhancedError): 'error' | 'warning' | 'info' {
if (error.isOpenAIError && error.errorDetails) {
if (error.isProviderError && error.errorDetails) {
switch (error.errorDetails.error_type) {
case 'quota_exhausted':
return 'error'; // Critical - user action required
@@ -200,12 +202,13 @@ export function getErrorSeverity(error: EnhancedError): 'error' | 'warning' | 'i
* Get suggested action for the user based on error type
*/
export function getErrorAction(error: EnhancedError): string | null {
if (error.isOpenAIError && error.errorDetails) {
if (error.isProviderError && error.errorDetails) {
const provider = error.errorDetails.provider ? error.errorDetails.provider.charAt(0).toUpperCase() + error.errorDetails.provider.slice(1) : 'LLM';
switch (error.errorDetails.error_type) {
case 'quota_exhausted':
return 'Check your OpenAI billing dashboard and add credits';
return `Check your ${provider} billing dashboard and add credits`;
case 'authentication_failed':
return 'Verify your OpenAI API key in Settings';
return `Verify your ${provider} API key in Settings`;
case 'rate_limit':
const retryAfter = error.errorDetails.retry_after;
if (retryAfter && retryAfter > 0) {
@@ -214,11 +217,11 @@ export function getErrorAction(error: EnhancedError): string | null {
return 'Wait a moment and try again';
}
case 'api_error':
return 'Verify your OpenAI API key in Settings';
return `Verify your ${provider} API key in Settings`;
case 'timeout_error':
return 'Check your network connection and try again';
case 'configuration_error':
return 'Check your OpenAI API key in Settings';
return `Check your ${provider} API key in Settings`;
default:
return null;
}