mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-01 04:09:08 -05:00
feat: Implement comprehensive OpenAI error handling for Issue #362
Replace silent failures with clear, actionable error messages to eliminate 90-minute debugging sessions when OpenAI API quota is exhausted. ## Backend Enhancements - Add error sanitization preventing sensitive data exposure (API keys, URLs, tokens) - Add upfront API key validation before expensive operations (crawl, upload, refresh) - Implement fail-fast pattern in RAG service (no more empty results for API failures) - Add specific error handling for quota, rate limit, auth, and API errors - Add EmbeddingAuthenticationError exception with masked key prefix support ## Frontend Enhancements - Create enhanced error utilities with OpenAI-specific parsing - Build TanStack Query compatible API wrapper preserving ETag caching - Update knowledge service to use enhanced error handling - Enhance TanStack Query hooks with user-friendly error messages ## Security Features - Comprehensive regex sanitization (8 patterns) with ReDoS protection - Input validation and circular reference detection - Generic fallback messages for sensitive keywords - Bounded quantifiers to prevent regex DoS attacks ## User Experience - Clear error messages: "OpenAI API quota exhausted" - Actionable guidance: "Check your OpenAI billing dashboard and add credits" - Immediate error visibility (no more silent failures) - Appropriate error severity styling ## Architecture Compatibility - Full TanStack Query integration maintained - ETag caching and optimistic updates preserved - No performance regression (all existing tests pass) - Compatible with existing knowledge base architecture Resolves #362: Users no longer experience mysterious empty RAG results that require extensive debugging to identify OpenAI quota issues. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { useActiveOperations } from "../progress/hooks";
|
||||
import { progressKeys } from "../progress/hooks/useProgressQueries";
|
||||
import type { ActiveOperation, ActiveOperationsResponse } from "../progress/types";
|
||||
import { knowledgeService } from "../services";
|
||||
import { getDisplayErrorMessage, type EnhancedError } from "../utils/errorHandler";
|
||||
import type {
|
||||
CrawlRequest,
|
||||
CrawlStartResponse,
|
||||
@@ -273,7 +274,10 @@ export function useCrawlUrl() {
|
||||
queryClient.setQueryData(progressKeys.list(), context.previousOperations);
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to start crawl";
|
||||
// Use enhanced error handling for better user experience
|
||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
||||
? getDisplayErrorMessage(error as EnhancedError)
|
||||
: (error instanceof Error ? error.message : "Failed to start crawl");
|
||||
showToast(errorMessage, "error");
|
||||
},
|
||||
});
|
||||
@@ -449,8 +453,10 @@ export function useUploadDocument() {
|
||||
queryClient.setQueryData(progressKeys.list(), context.previousOperations);
|
||||
}
|
||||
|
||||
// Display the actual error message from backend
|
||||
const message = error instanceof Error ? error.message : "Failed to upload document";
|
||||
// Use enhanced error handling for better user experience
|
||||
const message = (error as EnhancedError)?.isOpenAIError
|
||||
? getDisplayErrorMessage(error as EnhancedError)
|
||||
: (error instanceof Error ? error.message : "Failed to upload document");
|
||||
showToast(message, "error");
|
||||
},
|
||||
});
|
||||
@@ -521,7 +527,10 @@ export function useDeleteKnowledgeItem() {
|
||||
queryClient.setQueryData(queryKey, data);
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to delete item";
|
||||
// Use enhanced error handling for better user experience
|
||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
||||
? getDisplayErrorMessage(error as EnhancedError)
|
||||
: (error instanceof Error ? error.message : "Failed to delete item");
|
||||
showToast(errorMessage, "error");
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
@@ -568,7 +577,10 @@ export function useUpdateKnowledgeItem() {
|
||||
queryClient.setQueryData(knowledgeKeys.detail(variables.sourceId), context.previousItem);
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to update item";
|
||||
// Use enhanced error handling for better user experience
|
||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
||||
? getDisplayErrorMessage(error as EnhancedError)
|
||||
: (error instanceof Error ? error.message : "Failed to update item");
|
||||
showToast(errorMessage, "error");
|
||||
},
|
||||
onSuccess: (_data, { sourceId }) => {
|
||||
@@ -604,7 +616,10 @@ export function useRefreshKnowledgeItem() {
|
||||
return data;
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to refresh item";
|
||||
// Use enhanced error handling for better user experience
|
||||
const errorMessage = (error as EnhancedError)?.isOpenAIError
|
||||
? getDisplayErrorMessage(error as EnhancedError)
|
||||
: (error instanceof Error ? error.message : "Failed to refresh item");
|
||||
showToast(errorMessage, "error");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Enhanced API client for knowledge base operations with OpenAI error handling
|
||||
* Built on top of the ETag-aware API client with additional error parsing
|
||||
*/
|
||||
|
||||
import { callAPIWithETag } from "../../projects/shared/apiWithEtag";
|
||||
import { parseKnowledgeBaseError, type EnhancedError } from "../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* API call wrapper with enhanced OpenAI error handling
|
||||
* Uses ETag caching for efficiency while adding specialized error parsing
|
||||
*/
|
||||
export async function callKnowledgeAPI<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
try {
|
||||
// Use the ETag-aware API client for caching benefits
|
||||
return await callAPIWithETag<T>(endpoint, options);
|
||||
} catch (error: any) {
|
||||
// Apply enhanced error parsing for OpenAI errors
|
||||
const enhancedError = parseKnowledgeBaseError({
|
||||
status: error.statusCode || error.status,
|
||||
error: error.message || error.detail || error,
|
||||
detail: error.detail
|
||||
});
|
||||
|
||||
// Preserve the original error structure but enhance with our parsing
|
||||
const finalError = error as EnhancedError;
|
||||
finalError.isOpenAIError = enhancedError.isOpenAIError;
|
||||
finalError.errorDetails = enhancedError.errorDetails;
|
||||
finalError.message = enhancedError.message;
|
||||
|
||||
throw finalError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced upload wrapper that handles FormData and file uploads with better error handling
|
||||
*/
|
||||
export async function uploadWithEnhancedErrors(
|
||||
endpoint: string,
|
||||
formData: FormData,
|
||||
timeoutMs: number = 30000
|
||||
): Promise<any> {
|
||||
const API_BASE_URL = "/api"; // Use same base as other services
|
||||
|
||||
let fullUrl = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
// Handle test environment URLs
|
||||
if (typeof process !== "undefined" && process.env?.NODE_ENV === "test") {
|
||||
const testHost = process.env?.VITE_HOST || "localhost";
|
||||
const testPort = process.env?.ARCHON_SERVER_PORT || "8181";
|
||||
fullUrl = `http://${testHost}:${testPort}${fullUrl}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {
|
||||
const text = await response.text();
|
||||
errorData = { status: response.status, error: text };
|
||||
}
|
||||
|
||||
// Apply enhanced error parsing
|
||||
const enhancedError = parseKnowledgeBaseError({
|
||||
status: response.status,
|
||||
error: errorData.detail || errorData.error || errorData,
|
||||
detail: errorData.detail
|
||||
});
|
||||
|
||||
throw enhancedError;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error: any) {
|
||||
// Check if it's a timeout error
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
const timeoutError = parseKnowledgeBaseError(new Error('Request timed out'));
|
||||
throw timeoutError;
|
||||
}
|
||||
|
||||
// If it's already an enhanced error, re-throw it
|
||||
if (error && typeof error === 'object' && 'isOpenAIError' in error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Parse other errors through the error handler for consistency
|
||||
throw parseKnowledgeBaseError(error);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
* Handles all knowledge-related API operations using TanStack Query patterns
|
||||
*/
|
||||
|
||||
import { callAPIWithETag, invalidateETagCache } from "../../projects/shared/apiWithEtag";
|
||||
import { invalidateETagCache } from "../../projects/shared/apiWithEtag";
|
||||
import { callKnowledgeAPI, uploadWithEnhancedErrors } from "./apiWithEnhancedErrors";
|
||||
import type {
|
||||
ChunksResponse,
|
||||
CodeExamplesResponse,
|
||||
@@ -40,21 +41,21 @@ export const knowledgeService = {
|
||||
const queryString = params.toString();
|
||||
const endpoint = `/api/knowledge-items/summary${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
return callAPIWithETag<KnowledgeItemsResponse>(endpoint);
|
||||
return callKnowledgeAPI<KnowledgeItemsResponse>(endpoint);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific knowledge item
|
||||
*/
|
||||
async getKnowledgeItem(sourceId: string): Promise<KnowledgeItem> {
|
||||
return callAPIWithETag<KnowledgeItem>(`/api/knowledge-items/${sourceId}`);
|
||||
return callKnowledgeAPI<KnowledgeItem>(`/api/knowledge-items/${sourceId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a knowledge item
|
||||
*/
|
||||
async deleteKnowledgeItem(sourceId: string): Promise<{ success: boolean; message: string }> {
|
||||
const response = await callAPIWithETag<{ success: boolean; message: string }>(`/api/knowledge-items/${sourceId}`, {
|
||||
const response = await callKnowledgeAPI<{ success: boolean; message: string }>(`/api/knowledge-items/${sourceId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
@@ -70,7 +71,7 @@ export const knowledgeService = {
|
||||
* Update a knowledge item
|
||||
*/
|
||||
async updateKnowledgeItem(sourceId: string, updates: Partial<KnowledgeItem>): Promise<KnowledgeItem> {
|
||||
const response = await callAPIWithETag<KnowledgeItem>(`/api/knowledge-items/${sourceId}`, {
|
||||
const response = await callKnowledgeAPI<KnowledgeItem>(`/api/knowledge-items/${sourceId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
@@ -87,7 +88,7 @@ export const knowledgeService = {
|
||||
* Start crawling a URL
|
||||
*/
|
||||
async crawlUrl(request: CrawlRequest): Promise<CrawlStartResponse> {
|
||||
const response = await callAPIWithETag<CrawlStartResponse>("/api/knowledge-items/crawl", {
|
||||
const response = await callKnowledgeAPI<CrawlStartResponse>("/api/knowledge-items/crawl", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
@@ -103,7 +104,7 @@ export const knowledgeService = {
|
||||
* Refresh an existing knowledge item
|
||||
*/
|
||||
async refreshKnowledgeItem(sourceId: string): Promise<RefreshResponse> {
|
||||
const response = await callAPIWithETag<RefreshResponse>(`/api/knowledge-items/${sourceId}/refresh`, {
|
||||
const response = await callKnowledgeAPI<RefreshResponse>(`/api/knowledge-items/${sourceId}/refresh`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
@@ -132,38 +133,21 @@ export const knowledgeService = {
|
||||
formData.append("tags", JSON.stringify(metadata.tags));
|
||||
}
|
||||
|
||||
// Use fetch directly for file upload (FormData doesn't work well with our ETag wrapper)
|
||||
// In test environment, we need absolute URLs
|
||||
let uploadUrl = "/api/documents/upload";
|
||||
if (typeof process !== "undefined" && process.env?.NODE_ENV === "test") {
|
||||
const testHost = process.env?.VITE_HOST || "localhost";
|
||||
const testPort = process.env?.ARCHON_SERVER_PORT || "8181";
|
||||
uploadUrl = `http://${testHost}:${testPort}${uploadUrl}`;
|
||||
}
|
||||
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
signal: AbortSignal.timeout(30000), // 30 second timeout for file uploads
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || `HTTP ${response.status}`);
|
||||
}
|
||||
// Use enhanced upload wrapper with OpenAI error handling
|
||||
const result = await uploadWithEnhancedErrors("/documents/upload", formData, 30000);
|
||||
|
||||
// Invalidate list cache
|
||||
invalidateETagCache("/api/knowledge-items");
|
||||
invalidateETagCache("/api/knowledge-items/summary");
|
||||
|
||||
return response.json();
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop a running crawl
|
||||
*/
|
||||
async stopCrawl(progressId: string): Promise<{ success: boolean; message: string }> {
|
||||
return callAPIWithETag<{ success: boolean; message: string }>(`/api/knowledge-items/stop/${progressId}`, {
|
||||
return callKnowledgeAPI<{ success: boolean; message: string }>(`/api/knowledge-items/stop/${progressId}`, {
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
@@ -193,7 +177,7 @@ export const knowledgeService = {
|
||||
const queryString = params.toString();
|
||||
const endpoint = `/api/knowledge-items/${sourceId}/chunks${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
return callAPIWithETag<ChunksResponse>(endpoint);
|
||||
return callKnowledgeAPI<ChunksResponse>(endpoint);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -217,14 +201,14 @@ export const knowledgeService = {
|
||||
const queryString = params.toString();
|
||||
const endpoint = `/api/knowledge-items/${sourceId}/code-examples${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
return callAPIWithETag<CodeExamplesResponse>(endpoint);
|
||||
return callKnowledgeAPI<CodeExamplesResponse>(endpoint);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search the knowledge base
|
||||
*/
|
||||
async searchKnowledgeBase(options: SearchOptions): Promise<SearchResultsResponse> {
|
||||
return callAPIWithETag("/api/knowledge-items/search", {
|
||||
return callKnowledgeAPI("/api/knowledge-items/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
@@ -234,6 +218,6 @@ export const knowledgeService = {
|
||||
* Get available knowledge sources
|
||||
*/
|
||||
async getKnowledgeSources(): Promise<KnowledgeSource[]> {
|
||||
return callAPIWithETag<KnowledgeSource[]>("/api/knowledge-items/sources");
|
||||
return callKnowledgeAPI<KnowledgeSource[]>("/api/knowledge-items/sources");
|
||||
},
|
||||
};
|
||||
|
||||
208
archon-ui-main/src/features/knowledge/utils/errorHandler.ts
Normal file
208
archon-ui-main/src/features/knowledge/utils/errorHandler.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Error handling utilities for knowledge base operations
|
||||
*
|
||||
* Provides specialized error handling for OpenAI API errors,
|
||||
* rate limiting, and quota exhaustion scenarios.
|
||||
*
|
||||
* Related to GitHub issue #362 - improves user experience
|
||||
* by displaying clear error messages when OpenAI API fails.
|
||||
*/
|
||||
|
||||
export interface OpenAIErrorDetails {
|
||||
error: string;
|
||||
message: string;
|
||||
error_type: 'quota_exhausted' | 'rate_limit' | 'api_error' | 'authentication_failed';
|
||||
tokens_used?: number;
|
||||
retry_after?: number;
|
||||
api_key_prefix?: string;
|
||||
}
|
||||
|
||||
export interface EnhancedError extends Error {
|
||||
statusCode?: number;
|
||||
errorDetails?: OpenAIErrorDetails;
|
||||
isOpenAIError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fallback error for cases where input is invalid or unparseable
|
||||
*/
|
||||
function createFallbackError(reason: string): EnhancedError {
|
||||
return Object.assign(new Error('Unknown error occurred'), {
|
||||
errorDetails: {
|
||||
error: 'unknown',
|
||||
message: `${reason}. Please try again or contact support if the problem persists.`,
|
||||
error_type: 'api_error' as const
|
||||
}
|
||||
}) as EnhancedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an object can be safely serialized (no circular references)
|
||||
*/
|
||||
function isSafeObject(obj: any): boolean {
|
||||
try {
|
||||
JSON.stringify(obj);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and enhance API errors from knowledge base operations
|
||||
*/
|
||||
export function parseKnowledgeBaseError(error: any): EnhancedError {
|
||||
// Enhanced input validation
|
||||
if (!error) {
|
||||
return createFallbackError('No error information provided');
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return Object.assign(new Error(error), {
|
||||
errorDetails: {
|
||||
error: 'api_error',
|
||||
message: error,
|
||||
error_type: 'api_error' as const
|
||||
}
|
||||
}) as EnhancedError;
|
||||
}
|
||||
|
||||
if (typeof error !== 'object' || error === null) {
|
||||
return createFallbackError('Invalid error format');
|
||||
}
|
||||
|
||||
// Check for empty objects or objects with no useful properties
|
||||
if (error.constructor === Object && Object.keys(error).length === 0) {
|
||||
return createFallbackError('Empty error object received');
|
||||
}
|
||||
|
||||
// Check for circular references and object safety
|
||||
if (!isSafeObject(error)) {
|
||||
return createFallbackError('Error object contains circular references');
|
||||
}
|
||||
|
||||
// Handle Error instances that might have been serialized/deserialized
|
||||
if (error instanceof Error || (error.name && error.message && error.stack)) {
|
||||
// This is likely an Error object, proceed with parsing
|
||||
} else if (!error.message && !error.error && !error.detail && !error.status) {
|
||||
// Object doesn't have any recognizable error properties
|
||||
return createFallbackError('Unrecognized error object structure');
|
||||
}
|
||||
|
||||
const enhancedError: EnhancedError = new Error(error.message || 'Unknown error');
|
||||
|
||||
// Check if this is an HTTP response error with JSON details
|
||||
if (error && typeof error === 'object') {
|
||||
// Handle fetch Response errors
|
||||
if (error.status || error.statusCode) {
|
||||
enhancedError.statusCode = error.status || error.statusCode;
|
||||
}
|
||||
|
||||
// Parse error details from API response
|
||||
if (error.error || error.detail) {
|
||||
const errorData = error.error || error.detail;
|
||||
|
||||
// Check if it's an OpenAI-specific error
|
||||
if (typeof errorData === 'object' && errorData.error_type) {
|
||||
enhancedError.isOpenAIError = true;
|
||||
enhancedError.errorDetails = errorData as OpenAIErrorDetails;
|
||||
|
||||
// Override the message with the detailed error message
|
||||
enhancedError.message = errorData.message || errorData.error || enhancedError.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enhancedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error message for display in UI
|
||||
*/
|
||||
export function getDisplayErrorMessage(error: EnhancedError): string {
|
||||
if (error.isOpenAIError && error.errorDetails) {
|
||||
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.`;
|
||||
|
||||
case 'rate_limit':
|
||||
return `OpenAI 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.`;
|
||||
|
||||
case 'api_error':
|
||||
return `OpenAI API error: ${error.errorDetails.message}. Please check your API key configuration.`;
|
||||
|
||||
default:
|
||||
return error.errorDetails.message || error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle HTTP status codes
|
||||
if (error.statusCode) {
|
||||
switch (error.statusCode) {
|
||||
case 401:
|
||||
return 'Authentication failed. Please check your API key.';
|
||||
case 429:
|
||||
return 'API rate limit exceeded. Please wait a moment and try again.';
|
||||
case 502:
|
||||
return 'API service unavailable. Please try again in a few minutes.';
|
||||
case 503:
|
||||
return 'Service temporarily unavailable. Please try again later.';
|
||||
default:
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
return error.message || 'An unexpected error occurred.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error severity level for UI styling
|
||||
*/
|
||||
export function getErrorSeverity(error: EnhancedError): 'error' | 'warning' | 'info' {
|
||||
if (error.isOpenAIError && error.errorDetails) {
|
||||
switch (error.errorDetails.error_type) {
|
||||
case 'quota_exhausted':
|
||||
return 'error'; // Critical - user action required
|
||||
case 'authentication_failed':
|
||||
return 'error'; // Critical - configuration issue
|
||||
case 'rate_limit':
|
||||
return 'warning'; // Temporary - retry may work
|
||||
case 'api_error':
|
||||
return 'error'; // Likely configuration issue
|
||||
default:
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
if (error.statusCode && error.statusCode >= 500) {
|
||||
return 'error'; // Server errors
|
||||
}
|
||||
|
||||
return 'warning'; // Default to warning for other errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested action for the user based on error type
|
||||
*/
|
||||
export function getErrorAction(error: EnhancedError): string | null {
|
||||
if (error.isOpenAIError && error.errorDetails) {
|
||||
switch (error.errorDetails.error_type) {
|
||||
case 'quota_exhausted':
|
||||
return 'Check your OpenAI billing dashboard and add credits';
|
||||
case 'authentication_failed':
|
||||
return 'Verify your OpenAI API key in Settings';
|
||||
case 'rate_limit':
|
||||
const retryAfter = error.errorDetails.retry_after || 30;
|
||||
return `Wait ${retryAfter} seconds and try again`;
|
||||
case 'api_error':
|
||||
return 'Verify your OpenAI API key in Settings';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user