mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-01 04:09:08 -05:00
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:
@@ -275,7 +275,7 @@ export function useCrawlUrl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use enhanced error handling for better user experience
|
// Use enhanced error handling for better user experience
|
||||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
const errorMessage = (error as EnhancedError)?.isProviderError
|
||||||
? getDisplayErrorMessage(error as EnhancedError)
|
? getDisplayErrorMessage(error as EnhancedError)
|
||||||
: (error instanceof Error ? error.message : "Failed to start crawl");
|
: (error instanceof Error ? error.message : "Failed to start crawl");
|
||||||
|
|
||||||
@@ -455,7 +455,7 @@ export function useUploadDocument() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use enhanced error handling for better user experience
|
// Use enhanced error handling for better user experience
|
||||||
const message = (error as EnhancedError)?.isOpenAIError
|
const message = (error as EnhancedError)?.isProviderError
|
||||||
? getDisplayErrorMessage(error as EnhancedError)
|
? getDisplayErrorMessage(error as EnhancedError)
|
||||||
: (error instanceof Error ? error.message : "Failed to upload document");
|
: (error instanceof Error ? error.message : "Failed to upload document");
|
||||||
showToast(message, "error");
|
showToast(message, "error");
|
||||||
@@ -529,7 +529,7 @@ export function useDeleteKnowledgeItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use enhanced error handling for better user experience
|
// Use enhanced error handling for better user experience
|
||||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
const errorMessage = (error as EnhancedError)?.isProviderError
|
||||||
? getDisplayErrorMessage(error as EnhancedError)
|
? getDisplayErrorMessage(error as EnhancedError)
|
||||||
: (error instanceof Error ? error.message : "Failed to delete item");
|
: (error instanceof Error ? error.message : "Failed to delete item");
|
||||||
showToast(errorMessage, "error");
|
showToast(errorMessage, "error");
|
||||||
@@ -579,7 +579,7 @@ export function useUpdateKnowledgeItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use enhanced error handling for better user experience
|
// Use enhanced error handling for better user experience
|
||||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
const errorMessage = (error as EnhancedError)?.isProviderError
|
||||||
? getDisplayErrorMessage(error as EnhancedError)
|
? getDisplayErrorMessage(error as EnhancedError)
|
||||||
: (error instanceof Error ? error.message : "Failed to update item");
|
: (error instanceof Error ? error.message : "Failed to update item");
|
||||||
showToast(errorMessage, "error");
|
showToast(errorMessage, "error");
|
||||||
@@ -618,7 +618,7 @@ export function useRefreshKnowledgeItem() {
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
// Use enhanced error handling for better user experience
|
// Use enhanced error handling for better user experience
|
||||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
const errorMessage = (error as EnhancedError)?.isProviderError
|
||||||
? getDisplayErrorMessage(error as EnhancedError)
|
? getDisplayErrorMessage(error as EnhancedError)
|
||||||
: (error instanceof Error ? error.message : "Failed to refresh item");
|
: (error instanceof Error ? error.message : "Failed to refresh item");
|
||||||
showToast(errorMessage, "error");
|
showToast(errorMessage, "error");
|
||||||
|
|||||||
@@ -24,54 +24,63 @@ export async function callKnowledgeAPI<T>(
|
|||||||
// The ETag client extracts the error message but loses the structured details
|
// 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
|
// We need to reconstruct the structured error based on the status code and message
|
||||||
|
|
||||||
// Use status code and message patterns to identify OpenAI errors
|
// Detect provider from error message and use appropriate error structure
|
||||||
// More reliable than exact string matching
|
let provider = "LLM";
|
||||||
if (error.statusCode === 401 && error.message.includes("OpenAI API key")) {
|
if (error.message.includes("OpenAI")) provider = "OpenAI";
|
||||||
// This is our OpenAI authentication error
|
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 = {
|
errorData = {
|
||||||
status: 401,
|
status: 401,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
detail: {
|
detail: {
|
||||||
error: "Invalid OpenAI API key",
|
error: `Invalid ${provider} API key`,
|
||||||
message: "Please verify your OpenAI API key in Settings before starting a crawl.",
|
message: `Please verify your ${provider} API key in Settings before starting a crawl.`,
|
||||||
error_type: "authentication_failed",
|
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")) {
|
} else if (error.statusCode === 429 && error.message.toLowerCase().includes("quota")) {
|
||||||
// This is our OpenAI quota error
|
// Generic quota error
|
||||||
errorData = {
|
errorData = {
|
||||||
status: 429,
|
status: 429,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
detail: {
|
detail: {
|
||||||
error: "OpenAI quota exhausted",
|
error: `${provider} quota exhausted`,
|
||||||
message: "Your OpenAI API key has no remaining credits. Please add credits to your account.",
|
message: `Your ${provider} API quota has been exceeded. Please check your billing settings.`,
|
||||||
error_type: "quota_exhausted",
|
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")) {
|
} else if (error.statusCode === 429 && error.message.toLowerCase().includes("rate limit")) {
|
||||||
// This is our rate limit error
|
// Generic rate limit error
|
||||||
errorData = {
|
errorData = {
|
||||||
status: 429,
|
status: 429,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
detail: {
|
detail: {
|
||||||
error: "OpenAI API rate limit exceeded",
|
error: `${provider} API rate limit exceeded`,
|
||||||
message: "Too many requests to OpenAI API. Please wait a moment and try again.",
|
message: `Too many requests to ${provider} API. Please wait a moment and try again.`,
|
||||||
error_type: "rate_limit",
|
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")) {
|
} else if (error.statusCode === 502 && (error.message.toLowerCase().includes("api") || error.message.includes(provider))) {
|
||||||
// This is our generic API error
|
// Generic API error
|
||||||
errorData = {
|
errorData = {
|
||||||
status: 502,
|
status: 502,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
detail: {
|
detail: {
|
||||||
error: "OpenAI API error",
|
error: `${provider} API error`,
|
||||||
message: "OpenAI API error. Please check your API key configuration.",
|
message: `${provider} API error. Please check your API key configuration.`,
|
||||||
error_type: "api_error",
|
error_type: "api_error",
|
||||||
error_code: "OPENAI_API_ERROR"
|
error_code: `${provider.toUpperCase()}_API_ERROR`,
|
||||||
|
provider: provider.toLowerCase()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
* by displaying clear error messages when OpenAI API fails.
|
* by displaying clear error messages when OpenAI API fails.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface OpenAIErrorDetails {
|
export interface ProviderErrorDetails {
|
||||||
error: string;
|
error: string;
|
||||||
message: string;
|
message: string;
|
||||||
error_type: 'quota_exhausted' | 'rate_limit' | 'api_error' | 'authentication_failed' | 'timeout_error' | 'configuration_error';
|
error_type: 'quota_exhausted' | 'rate_limit' | 'api_error' | 'authentication_failed' | 'timeout_error' | 'configuration_error';
|
||||||
error_code?: string; // Structured error code for reliable detection
|
error_code?: string; // Structured error code for reliable detection
|
||||||
|
provider?: string; // LLM provider (openai, google, anthropic, ollama)
|
||||||
tokens_used?: number;
|
tokens_used?: number;
|
||||||
retry_after?: number;
|
retry_after?: number;
|
||||||
api_key_prefix?: string;
|
api_key_prefix?: string;
|
||||||
@@ -20,8 +21,8 @@ export interface OpenAIErrorDetails {
|
|||||||
|
|
||||||
export interface EnhancedError extends Error {
|
export interface EnhancedError extends Error {
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
errorDetails?: OpenAIErrorDetails;
|
errorDetails?: ProviderErrorDetails;
|
||||||
isOpenAIError?: boolean;
|
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)
|
// Prioritize error.detail (where we put structured OpenAI error data)
|
||||||
const errorData = error.detail || error.error;
|
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) {
|
if (typeof errorData === 'object' && errorData?.error_type) {
|
||||||
enhancedError.isOpenAIError = true;
|
enhancedError.isProviderError = true;
|
||||||
enhancedError.errorDetails = errorData as OpenAIErrorDetails;
|
enhancedError.errorDetails = errorData as ProviderErrorDetails;
|
||||||
|
|
||||||
// Override the message with the detailed error message
|
// Override the message with the detailed error message
|
||||||
enhancedError.message = errorData.message || errorData.error || enhancedError.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
|
* Get user-friendly error message for display in UI
|
||||||
*/
|
*/
|
||||||
export function getDisplayErrorMessage(error: EnhancedError): string {
|
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) {
|
switch (error.errorDetails.error_type) {
|
||||||
case 'quota_exhausted':
|
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':
|
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':
|
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':
|
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':
|
case 'timeout_error':
|
||||||
return `Request timed out. Please try again or check your network connection.`;
|
return `Request timed out. Please try again or check your network connection.`;
|
||||||
|
|
||||||
case 'configuration_error':
|
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:
|
default:
|
||||||
return error.errorDetails.message || error.message;
|
return error.errorDetails.message || error.message;
|
||||||
@@ -170,7 +172,7 @@ export function getDisplayErrorMessage(error: EnhancedError): string {
|
|||||||
* Get error severity level for UI styling
|
* Get error severity level for UI styling
|
||||||
*/
|
*/
|
||||||
export function getErrorSeverity(error: EnhancedError): 'error' | 'warning' | 'info' {
|
export function getErrorSeverity(error: EnhancedError): 'error' | 'warning' | 'info' {
|
||||||
if (error.isOpenAIError && error.errorDetails) {
|
if (error.isProviderError && error.errorDetails) {
|
||||||
switch (error.errorDetails.error_type) {
|
switch (error.errorDetails.error_type) {
|
||||||
case 'quota_exhausted':
|
case 'quota_exhausted':
|
||||||
return 'error'; // Critical - user action required
|
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
|
* Get suggested action for the user based on error type
|
||||||
*/
|
*/
|
||||||
export function getErrorAction(error: EnhancedError): string | null {
|
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) {
|
switch (error.errorDetails.error_type) {
|
||||||
case 'quota_exhausted':
|
case 'quota_exhausted':
|
||||||
return 'Check your OpenAI billing dashboard and add credits';
|
return `Check your ${provider} billing dashboard and add credits`;
|
||||||
case 'authentication_failed':
|
case 'authentication_failed':
|
||||||
return 'Verify your OpenAI API key in Settings';
|
return `Verify your ${provider} API key in Settings`;
|
||||||
case 'rate_limit':
|
case 'rate_limit':
|
||||||
const retryAfter = error.errorDetails.retry_after;
|
const retryAfter = error.errorDetails.retry_after;
|
||||||
if (retryAfter && retryAfter > 0) {
|
if (retryAfter && retryAfter > 0) {
|
||||||
@@ -214,11 +217,11 @@ export function getErrorAction(error: EnhancedError): string | null {
|
|||||||
return 'Wait a moment and try again';
|
return 'Wait a moment and try again';
|
||||||
}
|
}
|
||||||
case 'api_error':
|
case 'api_error':
|
||||||
return 'Verify your OpenAI API key in Settings';
|
return `Verify your ${provider} API key in Settings`;
|
||||||
case 'timeout_error':
|
case 'timeout_error':
|
||||||
return 'Check your network connection and try again';
|
return 'Check your network connection and try again';
|
||||||
case 'configuration_error':
|
case 'configuration_error':
|
||||||
return 'Check your OpenAI API key in Settings';
|
return `Check your ${provider} API key in Settings`;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,143 +53,128 @@ crawl_semaphore = asyncio.Semaphore(CONCURRENT_CRAWL_LIMIT)
|
|||||||
active_crawl_tasks: dict[str, asyncio.Task] = {}
|
active_crawl_tasks: dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_openai_error(error_message: str) -> str:
|
def _sanitize_provider_error(error_message: str, provider: str = None) -> str:
|
||||||
"""Sanitize OpenAI API error messages to prevent information disclosure."""
|
"""Sanitize provider-specific error messages to prevent information disclosure."""
|
||||||
import re
|
from ..services.embeddings.provider_error_adapters import ProviderErrorFactory
|
||||||
|
|
||||||
# Input validation
|
# Auto-detect provider if not specified
|
||||||
if not isinstance(error_message, str):
|
if not provider:
|
||||||
return "OpenAI API encountered an error. Please verify your API key and quota."
|
provider = ProviderErrorFactory.detect_provider_from_error(error_message)
|
||||||
if not error_message.strip():
|
|
||||||
return "OpenAI API encountered an error. Please verify your API key and quota."
|
|
||||||
|
|
||||||
# Length limit to prevent processing overly large error messages
|
# Use provider-specific sanitization
|
||||||
if len(error_message) > 2000:
|
return ProviderErrorFactory.sanitize_provider_error(error_message, provider)
|
||||||
return "OpenAI API encountered an error. Please verify your API key and quota."
|
|
||||||
|
|
||||||
# Optimized patterns using string operations where possible to prevent ReDoS
|
|
||||||
sanitized = error_message
|
|
||||||
|
|
||||||
# Use string operations for API key detection (faster and safer than regex)
|
|
||||||
if 'sk-' in sanitized:
|
|
||||||
words = sanitized.split()
|
|
||||||
for i, word in enumerate(words):
|
|
||||||
if word.startswith('sk-') and len(word) == 51: # OpenAI API key format: sk- + 48 chars
|
|
||||||
words[i] = '[REDACTED_KEY]'
|
|
||||||
sanitized = ' '.join(words)
|
|
||||||
|
|
||||||
# Use simple, efficient regex patterns with strict bounds
|
|
||||||
sanitized_patterns = [
|
|
||||||
(r'https?://[a-zA-Z0-9.-]+/[^\s]*', '[REDACTED_URL]'), # URLs with simplified pattern
|
|
||||||
(r'org-[a-zA-Z0-9]{24}', '[REDACTED_ORG]'), # Fixed length org IDs
|
|
||||||
(r'proj_[a-zA-Z0-9]{10,15}', '[REDACTED_PROJ]'), # Project IDs
|
|
||||||
(r'req_[a-zA-Z0-9]{6,15}', '[REDACTED_REQ]'), # Request IDs
|
|
||||||
(r'user-[a-zA-Z0-9]{10,15}', '[REDACTED_USER]'), # User IDs
|
|
||||||
(r'sess_[a-zA-Z0-9]{10,15}', '[REDACTED_SESS]'), # Session IDs
|
|
||||||
(r'Bearer [a-zA-Z0-9._-]+', 'Bearer [REDACTED_AUTH_TOKEN]'), # Bearer tokens
|
|
||||||
(r'"[^"]*auth[^"]*"', '[REDACTED_AUTH]'), # Auth details in quotes
|
|
||||||
]
|
|
||||||
|
|
||||||
# Apply patterns efficiently
|
|
||||||
for pattern, replacement in sanitized_patterns:
|
|
||||||
sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE)
|
|
||||||
|
|
||||||
# Check for sensitive words after pattern replacement
|
|
||||||
sensitive_words = ['internal', 'server', 'token']
|
|
||||||
# Only check for 'endpoint' if it's not part of our redacted URL pattern
|
|
||||||
if 'endpoint' in sanitized.lower() and '[REDACTED_URL]' not in sanitized:
|
|
||||||
sensitive_words.append('endpoint')
|
|
||||||
|
|
||||||
# Return generic message if still contains sensitive info
|
|
||||||
if any(word in sanitized.lower() for word in sensitive_words):
|
|
||||||
return "OpenAI API encountered an error. Please verify your API key and quota."
|
|
||||||
|
|
||||||
return sanitized
|
|
||||||
|
|
||||||
|
|
||||||
async def _validate_openai_api_key() -> None:
|
async def _validate_provider_api_key(provider: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
Validate OpenAI API key is present and working before starting operations.
|
Validate LLM provider API key is present and working before starting operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider: LLM provider name (openai, google, anthropic, ollama). If None, detects from active config.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 401 if API key is invalid/missing, 429 if quota exhausted
|
HTTPException: 401 if API key is invalid/missing, 429 if quota exhausted
|
||||||
"""
|
"""
|
||||||
# Import embedding exceptions for specific error handling
|
from ..services.embeddings.provider_error_adapters import ProviderErrorFactory
|
||||||
from ..services.embeddings.embedding_exceptions import (
|
|
||||||
EmbeddingAuthenticationError,
|
|
||||||
EmbeddingQuotaExhaustedError,
|
|
||||||
EmbeddingAPIError,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Get active provider if not specified
|
||||||
|
if not provider:
|
||||||
|
# Get current embedding provider from credentials
|
||||||
|
from ..services.credential_service import credential_service
|
||||||
|
provider_config = await credential_service.get_setting("EMBEDDING_PROVIDER", default="openai")
|
||||||
|
provider = provider_config.lower() if isinstance(provider_config, str) else "openai"
|
||||||
|
|
||||||
|
provider_name = ProviderErrorFactory.get_adapter(provider).get_provider_name()
|
||||||
|
logger.info(f"🔑 Validating {provider_name.title()} API key before starting operation...")
|
||||||
|
|
||||||
# Test the API key with a minimal embedding request
|
# Test the API key with a minimal embedding request
|
||||||
from ..services.embeddings.embedding_service import create_embedding
|
from ..services.embeddings.embedding_service import create_embedding
|
||||||
|
|
||||||
logger.info("🔑 Validating OpenAI API key before starting operation...")
|
|
||||||
# Try to create a test embedding with minimal content
|
|
||||||
test_result = await create_embedding(text="test")
|
test_result = await create_embedding(text="test")
|
||||||
|
|
||||||
if test_result:
|
if test_result:
|
||||||
logger.info("✅ OpenAI API key validation successful")
|
logger.info(f"✅ {provider_name.title()} API key validation successful")
|
||||||
else:
|
else:
|
||||||
logger.error("❌ OpenAI API key validation failed - no embedding returned")
|
logger.error(f"❌ {provider_name.title()} API key validation failed - no embedding returned")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail={
|
detail={
|
||||||
"error": "Invalid OpenAI API key",
|
"error": f"Invalid {provider_name.title()} API key",
|
||||||
"message": "Please verify your OpenAI API key in Settings before starting a crawl.",
|
"message": f"Please verify your {provider_name.title()} API key in Settings before starting a crawl.",
|
||||||
"error_type": "authentication_failed"
|
"error_type": "authentication_failed",
|
||||||
|
"error_code": f"{provider_name.upper()}_AUTH_FAILED",
|
||||||
|
"provider": provider_name
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
except EmbeddingAuthenticationError as e:
|
except EmbeddingAuthenticationError as e:
|
||||||
logger.error(f"❌ OpenAI authentication failed: {e}")
|
logger.error(f"❌ {provider_name.title()} authentication failed: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail={
|
detail={
|
||||||
"error": "Invalid OpenAI API key",
|
"error": f"Invalid {provider_name.title()} API key",
|
||||||
"message": "Please verify your OpenAI API key in Settings before starting a crawl.",
|
"message": f"Please verify your {provider_name.title()} API key in Settings before starting a crawl.",
|
||||||
"error_type": "authentication_failed",
|
"error_type": "authentication_failed",
|
||||||
"error_code": "OPENAI_AUTH_FAILED",
|
"error_code": f"{provider_name.upper()}_AUTH_FAILED",
|
||||||
|
"provider": provider_name,
|
||||||
"api_key_prefix": getattr(e, "api_key_prefix", None),
|
"api_key_prefix": getattr(e, "api_key_prefix", None),
|
||||||
}
|
}
|
||||||
) from None
|
) from None
|
||||||
except EmbeddingQuotaExhaustedError as e:
|
except EmbeddingQuotaExhaustedError as e:
|
||||||
logger.error(f"❌ OpenAI quota exhausted: {e}")
|
logger.error(f"❌ {provider_name.title()} quota exhausted: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
detail={
|
detail={
|
||||||
"error": "OpenAI quota exhausted",
|
"error": f"{provider_name.title()} quota exhausted",
|
||||||
"message": "Your OpenAI API key has no remaining credits. Please add credits to your account.",
|
"message": f"Your {provider_name.title()} API key has no remaining credits. Please add credits to your account.",
|
||||||
"error_type": "quota_exhausted",
|
"error_type": "quota_exhausted",
|
||||||
"error_code": "OPENAI_QUOTA_EXHAUSTED",
|
"error_code": f"{provider_name.upper()}_QUOTA_EXHAUSTED",
|
||||||
|
"provider": provider_name,
|
||||||
"tokens_used": getattr(e, "tokens_used", None),
|
"tokens_used": getattr(e, "tokens_used", None),
|
||||||
}
|
}
|
||||||
) from None
|
) from None
|
||||||
except EmbeddingAPIError as e:
|
except EmbeddingAPIError as e:
|
||||||
error_str = str(e)
|
error_str = str(e)
|
||||||
logger.error(f"❌ OpenAI API error during validation: {error_str}")
|
logger.error(f"❌ {provider_name.title()} API error during validation: {error_str}")
|
||||||
|
|
||||||
# Check if this is an authentication error (401 status code)
|
# Use provider-specific error parsing to determine the actual error type
|
||||||
if ("401" in error_str and ("invalid" in error_str.lower() or "incorrect" in error_str.lower())):
|
enhanced_error = ProviderErrorFactory.parse_provider_error(e, provider_name)
|
||||||
logger.error("🔍 Detected OpenAI authentication error in EmbeddingAPIError")
|
|
||||||
|
if isinstance(enhanced_error, EmbeddingAuthenticationError):
|
||||||
|
logger.error(f"🔍 Detected {provider_name.title()} authentication error in EmbeddingAPIError")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail={
|
detail={
|
||||||
"error": "Invalid OpenAI API key",
|
"error": f"Invalid {provider_name.title()} API key",
|
||||||
"message": "Please verify your OpenAI API key in Settings before starting a crawl.",
|
"message": f"Please verify your {provider_name.title()} API key in Settings before starting a crawl.",
|
||||||
"error_type": "authentication_failed"
|
"error_type": "authentication_failed",
|
||||||
|
"error_code": f"{provider_name.upper()}_AUTH_FAILED",
|
||||||
|
"provider": provider_name
|
||||||
|
}
|
||||||
|
) from None
|
||||||
|
elif isinstance(enhanced_error, EmbeddingQuotaExhaustedError):
|
||||||
|
logger.error(f"🔍 Detected {provider_name.title()} quota error in EmbeddingAPIError")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=429,
|
||||||
|
detail={
|
||||||
|
"error": f"{provider_name.title()} quota exhausted",
|
||||||
|
"message": f"Your {provider_name.title()} API quota has been exceeded. Please check your billing settings.",
|
||||||
|
"error_type": "quota_exhausted",
|
||||||
|
"error_code": f"{provider_name.upper()}_QUOTA_EXHAUSTED",
|
||||||
|
"provider": provider_name
|
||||||
}
|
}
|
||||||
) from None
|
) from None
|
||||||
else:
|
else:
|
||||||
# Other API errors should also block the operation
|
# Other API errors should also block the operation
|
||||||
logger.error("🔍 Other OpenAI API error during validation")
|
logger.error(f"🔍 Other {provider_name.title()} API error during validation")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=502,
|
status_code=502,
|
||||||
detail={
|
detail={
|
||||||
"error": "OpenAI API error",
|
"error": f"{provider_name.title()} API error",
|
||||||
"message": "OpenAI API error during validation. Please check your API key configuration.",
|
"message": f"{provider_name.title()} API error during validation. Please check your API key configuration.",
|
||||||
"error_type": "api_error"
|
"error_type": "api_error",
|
||||||
|
"error_code": f"{provider_name.upper()}_API_ERROR",
|
||||||
|
"provider": provider_name
|
||||||
}
|
}
|
||||||
) from None
|
) from None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -669,8 +654,8 @@ async def get_knowledge_item_code_examples(
|
|||||||
@router.post("/knowledge-items/{source_id}/refresh")
|
@router.post("/knowledge-items/{source_id}/refresh")
|
||||||
async def refresh_knowledge_item(source_id: str):
|
async def refresh_knowledge_item(source_id: str):
|
||||||
"""Refresh a knowledge item by re-crawling its URL with the same metadata."""
|
"""Refresh a knowledge item by re-crawling its URL with the same metadata."""
|
||||||
# CRITICAL: Validate OpenAI API key before starting refresh
|
# CRITICAL: Validate LLM provider API key before starting refresh
|
||||||
await _validate_openai_api_key()
|
await _validate_provider_api_key()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
safe_logfire_info(f"Starting knowledge item refresh | source_id={source_id}")
|
safe_logfire_info(f"Starting knowledge item refresh | source_id={source_id}")
|
||||||
@@ -790,8 +775,8 @@ async def crawl_knowledge_item(request: KnowledgeItemRequest):
|
|||||||
if not request.url.startswith(("http://", "https://")):
|
if not request.url.startswith(("http://", "https://")):
|
||||||
raise HTTPException(status_code=422, detail="URL must start with http:// or https://")
|
raise HTTPException(status_code=422, detail="URL must start with http:// or https://")
|
||||||
|
|
||||||
# CRITICAL: Validate OpenAI API key before starting crawl
|
# CRITICAL: Validate LLM provider API key before starting crawl
|
||||||
await _validate_openai_api_key()
|
await _validate_provider_api_key()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
safe_logfire_info(
|
safe_logfire_info(
|
||||||
@@ -946,8 +931,8 @@ async def upload_document(
|
|||||||
knowledge_type: str = Form("technical"),
|
knowledge_type: str = Form("technical"),
|
||||||
):
|
):
|
||||||
"""Upload and process a document with progress tracking."""
|
"""Upload and process a document with progress tracking."""
|
||||||
# CRITICAL: Validate OpenAI API key before starting upload
|
# CRITICAL: Validate LLM provider API key before starting upload
|
||||||
await _validate_openai_api_key()
|
await _validate_provider_api_key()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# DETAILED LOGGING: Track knowledge_type parameter flow
|
# DETAILED LOGGING: Track knowledge_type parameter flow
|
||||||
@@ -1181,61 +1166,77 @@ async def perform_rag_query(request: RagQueryRequest):
|
|||||||
EmbeddingRateLimitError,
|
EmbeddingRateLimitError,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle specific OpenAI/embedding errors with detailed messages
|
# Get current provider for error context
|
||||||
|
from ..services.embeddings.provider_error_adapters import ProviderErrorFactory
|
||||||
|
from ..services.credential_service import credential_service
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider_config = await credential_service.get_setting("EMBEDDING_PROVIDER", default="openai")
|
||||||
|
provider = provider_config.lower() if isinstance(provider_config, str) else "openai"
|
||||||
|
except Exception:
|
||||||
|
provider = "openai" # Fallback
|
||||||
|
|
||||||
|
provider_name = ProviderErrorFactory.get_adapter(provider).get_provider_name()
|
||||||
|
|
||||||
|
# Handle specific LLM provider embedding errors with detailed messages
|
||||||
if isinstance(e, EmbeddingAuthenticationError):
|
if isinstance(e, EmbeddingAuthenticationError):
|
||||||
safe_logfire_error(
|
safe_logfire_error(
|
||||||
f"OpenAI authentication failed during RAG query | query={request.query[:50]} | source={request.source}"
|
f"{provider_name.title()} authentication failed during RAG query | query={request.query[:50]} | source={request.source}"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail={
|
detail={
|
||||||
"error": "OpenAI API authentication failed",
|
"error": f"{provider_name.title()} API authentication failed",
|
||||||
"message": "Invalid or expired OpenAI API key. Please check your API key in settings.",
|
"message": f"Invalid or expired {provider_name.title()} API key. Please check your API key in settings.",
|
||||||
"error_type": "authentication_failed",
|
"error_type": "authentication_failed",
|
||||||
"error_code": "OPENAI_AUTH_FAILED",
|
"error_code": f"{provider_name.upper()}_AUTH_FAILED",
|
||||||
|
"provider": provider_name,
|
||||||
"api_key_prefix": getattr(e, "api_key_prefix", None),
|
"api_key_prefix": getattr(e, "api_key_prefix", None),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif isinstance(e, EmbeddingQuotaExhaustedError):
|
elif isinstance(e, EmbeddingQuotaExhaustedError):
|
||||||
safe_logfire_error(
|
safe_logfire_error(
|
||||||
f"OpenAI quota exhausted during RAG query | query={request.query[:50]} | source={request.source}"
|
f"{provider_name.title()} quota exhausted during RAG query | query={request.query[:50]} | source={request.source}"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
detail={
|
detail={
|
||||||
"error": "OpenAI API quota exhausted",
|
"error": f"{provider_name.title()} API quota exhausted",
|
||||||
"message": "Your OpenAI API key has no remaining credits. Please add credits to your OpenAI account or check your billing settings.",
|
"message": f"Your {provider_name.title()} API quota has been exceeded. Please check your billing settings.",
|
||||||
"error_type": "quota_exhausted",
|
"error_type": "quota_exhausted",
|
||||||
"error_code": "OPENAI_QUOTA_EXHAUSTED",
|
"error_code": f"{provider_name.upper()}_QUOTA_EXHAUSTED",
|
||||||
|
"provider": provider_name,
|
||||||
"tokens_used": getattr(e, "tokens_used", None),
|
"tokens_used": getattr(e, "tokens_used", None),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif isinstance(e, EmbeddingRateLimitError):
|
elif isinstance(e, EmbeddingRateLimitError):
|
||||||
safe_logfire_error(
|
safe_logfire_error(
|
||||||
f"OpenAI rate limit hit during RAG query | query={request.query[:50]} | source={request.source}"
|
f"{provider_name.title()} rate limit hit during RAG query | query={request.query[:50]} | source={request.source}"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
detail={
|
detail={
|
||||||
"error": "OpenAI API rate limit exceeded",
|
"error": f"{provider_name.title()} API rate limit exceeded",
|
||||||
"message": "Too many requests to OpenAI API. Please wait a moment and try again.",
|
"message": f"Too many requests to {provider_name.title()} API. Please wait a moment and try again.",
|
||||||
"error_type": "rate_limit",
|
"error_type": "rate_limit",
|
||||||
"error_code": "OPENAI_RATE_LIMIT",
|
"error_code": f"{provider_name.upper()}_RATE_LIMIT",
|
||||||
|
"provider": provider_name,
|
||||||
"retry_after": 30, # Suggest 30 second wait
|
"retry_after": 30, # Suggest 30 second wait
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif isinstance(e, EmbeddingAPIError):
|
elif isinstance(e, EmbeddingAPIError):
|
||||||
safe_logfire_error(
|
safe_logfire_error(
|
||||||
f"OpenAI API error during RAG query | error={str(e)} | query={request.query[:50]} | source={request.source}"
|
f"{provider_name.title()} API error during RAG query | error={str(e)} | query={request.query[:50]} | source={request.source}"
|
||||||
)
|
)
|
||||||
sanitized_message = _sanitize_openai_error(str(e))
|
sanitized_message = _sanitize_provider_error(str(e), provider_name)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=502,
|
status_code=502,
|
||||||
detail={
|
detail={
|
||||||
"error": "OpenAI API error",
|
"error": f"{provider_name.title()} API error",
|
||||||
"message": f"OpenAI API error: {sanitized_message}",
|
"message": f"{provider_name.title()} API error: {sanitized_message}",
|
||||||
"error_type": "api_error",
|
"error_type": "api_error",
|
||||||
"error_code": "OPENAI_API_ERROR",
|
"error_code": f"{provider_name.upper()}_API_ERROR",
|
||||||
|
"provider": provider_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
374
python/src/server/services/embeddings/provider_error_adapters.py
Normal file
374
python/src/server/services/embeddings/provider_error_adapters.py
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
"""
|
||||||
|
Provider-specific error handling adapters for embedding services.
|
||||||
|
|
||||||
|
This module provides a unified interface for handling errors from different
|
||||||
|
LLM providers (OpenAI, Google AI, Anthropic, Ollama, etc.) while maintaining
|
||||||
|
provider-specific error parsing and sanitization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .embedding_exceptions import (
|
||||||
|
EmbeddingAPIError,
|
||||||
|
EmbeddingAuthenticationError,
|
||||||
|
EmbeddingQuotaExhaustedError,
|
||||||
|
EmbeddingRateLimitError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderErrorAdapter(ABC):
|
||||||
|
"""Abstract base class for provider-specific error handling."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_provider_name(self) -> str:
|
||||||
|
"""Return the provider name for this adapter."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_error(self, error: Exception) -> Exception:
|
||||||
|
"""Parse provider-specific error into standard embedding exception."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def sanitize_error_message(self, message: str) -> str:
|
||||||
|
"""Sanitize provider-specific sensitive data from error messages."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_api_key_formats(self) -> list[str]:
|
||||||
|
"""Return regex patterns for detecting this provider's API keys."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIErrorAdapter(ProviderErrorAdapter):
|
||||||
|
"""Error adapter for OpenAI API errors."""
|
||||||
|
|
||||||
|
def get_provider_name(self) -> str:
|
||||||
|
return "openai"
|
||||||
|
|
||||||
|
def parse_error(self, error: Exception) -> Exception:
|
||||||
|
"""Parse OpenAI-specific errors into standard embedding exceptions."""
|
||||||
|
error_str = str(error)
|
||||||
|
|
||||||
|
# Handle OpenAI authentication errors
|
||||||
|
if ("401" in error_str and ("invalid" in error_str.lower() or "incorrect" in error_str.lower())):
|
||||||
|
# Extract API key prefix if available
|
||||||
|
api_key_prefix = None
|
||||||
|
if "sk-" in error_str:
|
||||||
|
import re
|
||||||
|
key_match = re.search(r'sk-([a-zA-Z0-9]{3})', error_str)
|
||||||
|
if key_match:
|
||||||
|
api_key_prefix = f"sk-{key_match.group(1)}…"
|
||||||
|
|
||||||
|
return EmbeddingAuthenticationError(
|
||||||
|
"Invalid OpenAI API key",
|
||||||
|
api_key_prefix=api_key_prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle quota exhaustion
|
||||||
|
elif ("quota" in error_str.lower() or "billing" in error_str.lower() or "credits" in error_str.lower()):
|
||||||
|
# Try to extract token usage if available
|
||||||
|
tokens_used = None
|
||||||
|
token_match = re.search(r'(\d+)\s*tokens?', error_str, re.IGNORECASE)
|
||||||
|
if token_match:
|
||||||
|
tokens_used = int(token_match.group(1))
|
||||||
|
|
||||||
|
return EmbeddingQuotaExhaustedError(
|
||||||
|
"OpenAI quota exhausted",
|
||||||
|
tokens_used=tokens_used
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle rate limiting
|
||||||
|
elif ("rate" in error_str.lower() and "limit" in error_str.lower()):
|
||||||
|
return EmbeddingRateLimitError("OpenAI rate limit exceeded")
|
||||||
|
|
||||||
|
# Generic API error
|
||||||
|
else:
|
||||||
|
return EmbeddingAPIError(f"OpenAI API error: {error_str}", original_error=error)
|
||||||
|
|
||||||
|
def sanitize_error_message(self, message: str) -> str:
|
||||||
|
"""Sanitize OpenAI-specific sensitive data."""
|
||||||
|
if not isinstance(message, str) or not message.strip():
|
||||||
|
return "OpenAI API encountered an error. Please verify your API key and quota."
|
||||||
|
|
||||||
|
if len(message) > 2000:
|
||||||
|
return "OpenAI API encountered an error. Please verify your API key and quota."
|
||||||
|
|
||||||
|
sanitized = message
|
||||||
|
|
||||||
|
# Use string operations for API key detection (OpenAI format: sk-...)
|
||||||
|
if 'sk-' in sanitized:
|
||||||
|
words = sanitized.split()
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
if word.startswith('sk-') and len(word) == 51:
|
||||||
|
words[i] = '[REDACTED_KEY]'
|
||||||
|
sanitized = ' '.join(words)
|
||||||
|
|
||||||
|
# OpenAI-specific patterns
|
||||||
|
patterns = [
|
||||||
|
(r'https?://[a-zA-Z0-9.-]+/[^\s]*', '[REDACTED_URL]'),
|
||||||
|
(r'org-[a-zA-Z0-9]{24}', '[REDACTED_ORG]'),
|
||||||
|
(r'proj_[a-zA-Z0-9]{10,15}', '[REDACTED_PROJ]'),
|
||||||
|
(r'req_[a-zA-Z0-9]{6,15}', '[REDACTED_REQ]'),
|
||||||
|
(r'Bearer [a-zA-Z0-9._-]+', 'Bearer [REDACTED_AUTH_TOKEN]'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, replacement in patterns:
|
||||||
|
sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
# Check for sensitive words
|
||||||
|
sensitive_words = ['internal', 'server', 'token']
|
||||||
|
if 'endpoint' in sanitized.lower() and '[REDACTED_URL]' not in sanitized:
|
||||||
|
sensitive_words.append('endpoint')
|
||||||
|
|
||||||
|
if any(word in sanitized.lower() for word in sensitive_words):
|
||||||
|
return "OpenAI API encountered an error. Please verify your API key and quota."
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
|
||||||
|
def get_api_key_formats(self) -> list[str]:
|
||||||
|
return [r'sk-[a-zA-Z0-9]{48}']
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleAIErrorAdapter(ProviderErrorAdapter):
|
||||||
|
"""Error adapter for Google AI API errors."""
|
||||||
|
|
||||||
|
def get_provider_name(self) -> str:
|
||||||
|
return "google"
|
||||||
|
|
||||||
|
def parse_error(self, error: Exception) -> Exception:
|
||||||
|
"""Parse Google AI-specific errors into standard embedding exceptions."""
|
||||||
|
error_str = str(error)
|
||||||
|
|
||||||
|
# Handle Google AI authentication errors
|
||||||
|
if ("403" in error_str or "401" in error_str) and ("api" in error_str.lower() and "key" in error_str.lower()):
|
||||||
|
# Extract API key prefix if available
|
||||||
|
api_key_prefix = None
|
||||||
|
if "AIza" in error_str:
|
||||||
|
key_match = re.search(r'AIza([a-zA-Z0-9]{4})', error_str)
|
||||||
|
if key_match:
|
||||||
|
api_key_prefix = f"AIza{key_match.group(1)}…"
|
||||||
|
|
||||||
|
return EmbeddingAuthenticationError(
|
||||||
|
"Invalid Google AI API key",
|
||||||
|
api_key_prefix=api_key_prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle quota/billing issues
|
||||||
|
elif ("quota" in error_str.lower() or "exceeded" in error_str.lower() or "billing" in error_str.lower()):
|
||||||
|
return EmbeddingQuotaExhaustedError("Google AI quota exceeded")
|
||||||
|
|
||||||
|
# Handle rate limiting
|
||||||
|
elif ("rate" in error_str.lower() and "limit" in error_str.lower()):
|
||||||
|
return EmbeddingRateLimitError("Google AI rate limit exceeded")
|
||||||
|
|
||||||
|
# Generic API error
|
||||||
|
else:
|
||||||
|
return EmbeddingAPIError(f"Google AI API error: {error_str}", original_error=error)
|
||||||
|
|
||||||
|
def sanitize_error_message(self, message: str) -> str:
|
||||||
|
"""Sanitize Google AI-specific sensitive data."""
|
||||||
|
if not isinstance(message, str) or not message.strip():
|
||||||
|
return "Google AI API encountered an error. Please verify your API key and quota."
|
||||||
|
|
||||||
|
if len(message) > 2000:
|
||||||
|
return "Google AI API encountered an error. Please verify your API key and quota."
|
||||||
|
|
||||||
|
sanitized = message
|
||||||
|
|
||||||
|
# Google AI API key format: AIzaSy...
|
||||||
|
if 'AIza' in sanitized:
|
||||||
|
words = sanitized.split()
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
if word.startswith('AIza') and len(word) == 39: # Google AI key format
|
||||||
|
words[i] = '[REDACTED_KEY]'
|
||||||
|
sanitized = ' '.join(words)
|
||||||
|
|
||||||
|
# Google AI-specific patterns
|
||||||
|
patterns = [
|
||||||
|
(r'https?://[a-zA-Z0-9.-]*googleapis\.com[^\s]*', '[REDACTED_URL]'),
|
||||||
|
(r'projects/[a-zA-Z0-9_-]+', 'projects/[REDACTED_PROJECT]'),
|
||||||
|
(r'Bearer [a-zA-Z0-9._-]+', 'Bearer [REDACTED_AUTH_TOKEN]'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, replacement in patterns:
|
||||||
|
sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
# Check for Google AI sensitive words
|
||||||
|
sensitive_words = ['internal', 'server', 'token', 'project']
|
||||||
|
if any(word in sanitized.lower() for word in sensitive_words):
|
||||||
|
return "Google AI API encountered an error. Please verify your API key and quota."
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
|
||||||
|
def get_api_key_formats(self) -> list[str]:
|
||||||
|
return [r'AIza[a-zA-Z0-9]{35}']
|
||||||
|
|
||||||
|
|
||||||
|
class AnthropicErrorAdapter(ProviderErrorAdapter):
|
||||||
|
"""Error adapter for Anthropic API errors."""
|
||||||
|
|
||||||
|
def get_provider_name(self) -> str:
|
||||||
|
return "anthropic"
|
||||||
|
|
||||||
|
def parse_error(self, error: Exception) -> Exception:
|
||||||
|
"""Parse Anthropic-specific errors into standard embedding exceptions."""
|
||||||
|
error_str = str(error)
|
||||||
|
|
||||||
|
# Handle Anthropic authentication errors
|
||||||
|
if ("401" in error_str or "403" in error_str) and ("api" in error_str.lower() and "key" in error_str.lower()):
|
||||||
|
api_key_prefix = None
|
||||||
|
if "sk-ant" in error_str:
|
||||||
|
key_match = re.search(r'sk-ant-([a-zA-Z0-9]{6})', error_str)
|
||||||
|
if key_match:
|
||||||
|
api_key_prefix = f"sk-ant-{key_match.group(1)}…"
|
||||||
|
|
||||||
|
return EmbeddingAuthenticationError(
|
||||||
|
"Invalid Anthropic API key",
|
||||||
|
api_key_prefix=api_key_prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle quota/billing issues
|
||||||
|
elif ("quota" in error_str.lower() or "billing" in error_str.lower() or "usage" in error_str.lower()):
|
||||||
|
return EmbeddingQuotaExhaustedError("Anthropic quota exceeded")
|
||||||
|
|
||||||
|
# Handle rate limiting
|
||||||
|
elif ("rate" in error_str.lower() and "limit" in error_str.lower()):
|
||||||
|
return EmbeddingRateLimitError("Anthropic rate limit exceeded")
|
||||||
|
|
||||||
|
# Generic API error
|
||||||
|
else:
|
||||||
|
return EmbeddingAPIError(f"Anthropic API error: {error_str}", original_error=error)
|
||||||
|
|
||||||
|
def sanitize_error_message(self, message: str) -> str:
|
||||||
|
"""Sanitize Anthropic-specific sensitive data."""
|
||||||
|
if not isinstance(message, str) or not message.strip():
|
||||||
|
return "Anthropic API encountered an error. Please verify your API key."
|
||||||
|
|
||||||
|
if len(message) > 2000:
|
||||||
|
return "Anthropic API encountered an error. Please verify your API key."
|
||||||
|
|
||||||
|
sanitized = message
|
||||||
|
|
||||||
|
# Anthropic API key format: sk-ant-...
|
||||||
|
if 'sk-ant-' in sanitized:
|
||||||
|
words = sanitized.split()
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
if word.startswith('sk-ant-') and len(word) > 20:
|
||||||
|
words[i] = '[REDACTED_KEY]'
|
||||||
|
sanitized = ' '.join(words)
|
||||||
|
|
||||||
|
# Anthropic-specific patterns
|
||||||
|
patterns = [
|
||||||
|
(r'https?://[a-zA-Z0-9.-]*anthropic\.com[^\s]*', '[REDACTED_URL]'),
|
||||||
|
(r'Bearer [a-zA-Z0-9._-]+', 'Bearer [REDACTED_AUTH_TOKEN]'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, replacement in patterns:
|
||||||
|
sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
# Check for sensitive words
|
||||||
|
sensitive_words = ['internal', 'server', 'token']
|
||||||
|
if any(word in sanitized.lower() for word in sensitive_words):
|
||||||
|
return "Anthropic API encountered an error. Please verify your API key."
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
|
||||||
|
def get_api_key_formats(self) -> list[str]:
|
||||||
|
return [r'sk-ant-[a-zA-Z0-9_-]+']
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaErrorAdapter(ProviderErrorAdapter):
|
||||||
|
"""Error adapter for Ollama (local) errors."""
|
||||||
|
|
||||||
|
def get_provider_name(self) -> str:
|
||||||
|
return "ollama"
|
||||||
|
|
||||||
|
def parse_error(self, error: Exception) -> Exception:
|
||||||
|
"""Parse Ollama-specific errors into standard embedding exceptions."""
|
||||||
|
error_str = str(error)
|
||||||
|
|
||||||
|
# Ollama is typically local, so auth errors are usually connection issues
|
||||||
|
if ("connection" in error_str.lower() or "refused" in error_str.lower()):
|
||||||
|
return EmbeddingAuthenticationError("Cannot connect to Ollama server")
|
||||||
|
|
||||||
|
# Ollama doesn't have quotas, but may have model issues
|
||||||
|
elif ("model" in error_str.lower() and ("not found" in error_str.lower() or "not available" in error_str.lower())):
|
||||||
|
return EmbeddingAPIError(f"Ollama model error: {error_str}", original_error=error)
|
||||||
|
|
||||||
|
# Generic error
|
||||||
|
else:
|
||||||
|
return EmbeddingAPIError(f"Ollama error: {error_str}", original_error=error)
|
||||||
|
|
||||||
|
def sanitize_error_message(self, message: str) -> str:
|
||||||
|
"""Sanitize Ollama-specific sensitive data."""
|
||||||
|
if not isinstance(message, str) or not message.strip():
|
||||||
|
return "Ollama service encountered an error. Please check your Ollama configuration."
|
||||||
|
|
||||||
|
# Ollama doesn't use API keys, but may expose local paths or URLs
|
||||||
|
sanitized = message
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
(r'http://localhost:\d+', '[REDACTED_LOCAL_URL]'),
|
||||||
|
(r'/[a-zA-Z0-9/_.-]+', '[REDACTED_PATH]'), # Local file paths
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, replacement in patterns:
|
||||||
|
sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
|
||||||
|
def get_api_key_formats(self) -> list[str]:
|
||||||
|
return [] # Ollama doesn't use API keys
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderErrorFactory:
|
||||||
|
"""Factory for provider-specific error handling."""
|
||||||
|
|
||||||
|
_adapters = {
|
||||||
|
"openai": OpenAIErrorAdapter(),
|
||||||
|
"google": GoogleAIErrorAdapter(),
|
||||||
|
"anthropic": AnthropicErrorAdapter(),
|
||||||
|
"ollama": OllamaErrorAdapter(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_adapter(cls, provider: str) -> ProviderErrorAdapter:
|
||||||
|
"""Get error adapter for the specified provider."""
|
||||||
|
return cls._adapters.get(provider.lower(), cls._adapters["openai"])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_provider_error(cls, error: Exception, provider: str) -> Exception:
|
||||||
|
"""Parse provider-specific error using appropriate adapter."""
|
||||||
|
adapter = cls.get_adapter(provider)
|
||||||
|
return adapter.parse_error(error)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sanitize_provider_error(cls, message: str, provider: str) -> str:
|
||||||
|
"""Sanitize error message using provider-specific adapter."""
|
||||||
|
adapter = cls.get_adapter(provider)
|
||||||
|
return adapter.sanitize_error_message(message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_supported_providers(cls) -> list[str]:
|
||||||
|
"""Get list of supported providers."""
|
||||||
|
return list(cls._adapters.keys())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect_provider_from_error(cls, error_str: str) -> str:
|
||||||
|
"""Attempt to detect provider from error message content."""
|
||||||
|
error_lower = error_str.lower()
|
||||||
|
|
||||||
|
# Check for provider-specific patterns in order of specificity
|
||||||
|
if "anthropic" in error_lower or "sk-ant-" in error_str:
|
||||||
|
return "anthropic"
|
||||||
|
elif "google" in error_lower or "googleapis" in error_lower or "AIza" in error_str:
|
||||||
|
return "google"
|
||||||
|
elif "ollama" in error_lower or "localhost" in error_lower:
|
||||||
|
return "ollama"
|
||||||
|
elif "openai" in error_lower or "sk-" in error_str:
|
||||||
|
return "openai"
|
||||||
|
else:
|
||||||
|
return "openai" # Default fallback
|
||||||
Reference in New Issue
Block a user