feat: enhance error handling for LLM API calls

Add LLM error alert functionality to display specific error messages based on API responses. Introduce new LlmErrorAlertType interface for structured error alerts. Update chat components to manage and display LLM error alerts effectively, improving user feedback during error scenarios.
This commit is contained in:
xKevIsDev
2025-07-03 11:43:58 +01:00
parent 46611a8172
commit 9d6ff741d9
6 changed files with 256 additions and 25 deletions

View File

@@ -19,7 +19,7 @@ import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
import GitCloneButton from './GitCloneButton'; import GitCloneButton from './GitCloneButton';
import type { ProviderInfo } from '~/types/model'; import type { ProviderInfo } from '~/types/model';
import StarterTemplates from './StarterTemplates'; import StarterTemplates from './StarterTemplates';
import type { ActionAlert, SupabaseAlert, DeployAlert } from '~/types/actions'; import type { ActionAlert, SupabaseAlert, DeployAlert, LlmErrorAlertType } from '~/types/actions';
import DeployChatAlert from '~/components/deploy/DeployAlert'; import DeployChatAlert from '~/components/deploy/DeployAlert';
import ChatAlert from './ChatAlert'; import ChatAlert from './ChatAlert';
import type { ModelInfo } from '~/lib/modules/llm/types'; import type { ModelInfo } from '~/lib/modules/llm/types';
@@ -32,6 +32,7 @@ import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
import { ChatBox } from './ChatBox'; import { ChatBox } from './ChatBox';
import type { DesignScheme } from '~/types/design-scheme'; import type { DesignScheme } from '~/types/design-scheme';
import type { ElementInfo } from '~/components/workbench/Inspector'; import type { ElementInfo } from '~/components/workbench/Inspector';
import LlmErrorAlert from './LLMApiAlert';
const TEXTAREA_MIN_HEIGHT = 76; const TEXTAREA_MIN_HEIGHT = 76;
@@ -69,6 +70,8 @@ interface BaseChatProps {
clearSupabaseAlert?: () => void; clearSupabaseAlert?: () => void;
deployAlert?: DeployAlert; deployAlert?: DeployAlert;
clearDeployAlert?: () => void; clearDeployAlert?: () => void;
llmErrorAlert?: LlmErrorAlertType;
clearLlmErrorAlert?: () => void;
data?: JSONValue[] | undefined; data?: JSONValue[] | undefined;
chatMode?: 'discuss' | 'build'; chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void; setChatMode?: (mode: 'discuss' | 'build') => void;
@@ -113,6 +116,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
clearDeployAlert, clearDeployAlert,
supabaseAlert, supabaseAlert,
clearSupabaseAlert, clearSupabaseAlert,
llmErrorAlert,
clearLlmErrorAlert,
data, data,
chatMode, chatMode,
setChatMode, setChatMode,
@@ -411,6 +416,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}} }}
/> />
)} )}
{llmErrorAlert && <LlmErrorAlert alert={llmErrorAlert} clearAlert={() => clearLlmErrorAlert?.()} />}
</div> </div>
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />} {progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
<ChatBox <ChatBox

View File

@@ -29,6 +29,7 @@ import { filesToArtifacts } from '~/utils/fileUtils';
import { supabaseConnection } from '~/lib/stores/supabase'; import { supabaseConnection } from '~/lib/stores/supabase';
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme'; import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
import type { ElementInfo } from '~/components/workbench/Inspector'; import type { ElementInfo } from '~/components/workbench/Inspector';
import type { LlmErrorAlertType } from '~/types/actions';
const toastAnimation = cssTransition({ const toastAnimation = cssTransition({
enter: 'animated fadeInRight', enter: 'animated fadeInRight',
@@ -129,12 +130,13 @@ export const ChatImpl = memo(
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme); const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
const actionAlert = useStore(workbenchStore.alert); const actionAlert = useStore(workbenchStore.alert);
const deployAlert = useStore(workbenchStore.deployAlert); const deployAlert = useStore(workbenchStore.deployAlert);
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection const supabaseConn = useStore(supabaseConnection);
const selectedProject = supabaseConn.stats?.projects?.find( const selectedProject = supabaseConn.stats?.projects?.find(
(project) => project.id === supabaseConn.selectedProjectId, (project) => project.id === supabaseConn.selectedProjectId,
); );
const supabaseAlert = useStore(workbenchStore.supabaseAlert); const supabaseAlert = useStore(workbenchStore.supabaseAlert);
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings(); const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
const [llmErrorAlert, setLlmErrorAlert] = useState<LlmErrorAlertType | undefined>(undefined);
const [model, setModel] = useState(() => { const [model, setModel] = useState(() => {
const savedModel = Cookies.get('selectedModel'); const savedModel = Cookies.get('selectedModel');
return savedModel || DEFAULT_MODEL; return savedModel || DEFAULT_MODEL;
@@ -181,15 +183,8 @@ export const ChatImpl = memo(
}, },
sendExtraMessageFields: true, sendExtraMessageFields: true,
onError: (e) => { onError: (e) => {
logger.error('Request failed\n\n', e, error); setFakeLoading(false);
logStore.logError('Chat request failed', e, { handleError(e, 'chat');
component: 'Chat',
action: 'request',
error: e.message,
});
toast.error(
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
);
}, },
onFinish: (message, response) => { onFinish: (message, response) => {
const usage = response.usage; const usage = response.usage;
@@ -272,6 +267,80 @@ export const ChatImpl = memo(
}); });
}; };
const handleError = useCallback(
(error: any, context: 'chat' | 'template' | 'llmcall' = 'chat') => {
logger.error(`${context} request failed`, error);
stop();
setFakeLoading(false);
let errorInfo = {
message: 'An unexpected error occurred',
isRetryable: true,
statusCode: 500,
provider: provider.name,
type: 'unknown' as const,
retryDelay: 0,
};
if (error.message) {
try {
const parsed = JSON.parse(error.message);
if (parsed.error || parsed.message) {
errorInfo = { ...errorInfo, ...parsed };
} else {
errorInfo.message = error.message;
}
} catch {
errorInfo.message = error.message;
}
}
let errorType: LlmErrorAlertType['errorType'] = 'unknown';
let title = 'Request Failed';
if (errorInfo.statusCode === 401 || errorInfo.message.toLowerCase().includes('api key')) {
errorType = 'authentication';
title = 'Authentication Error';
} else if (errorInfo.statusCode === 429 || errorInfo.message.toLowerCase().includes('rate limit')) {
errorType = 'rate_limit';
title = 'Rate Limit Exceeded';
} else if (errorInfo.message.toLowerCase().includes('quota')) {
errorType = 'quota';
title = 'Quota Exceeded';
} else if (errorInfo.statusCode >= 500) {
errorType = 'network';
title = 'Server Error';
}
logStore.logError(`${context} request failed`, error, {
component: 'Chat',
action: 'request',
error: errorInfo.message,
context,
retryable: errorInfo.isRetryable,
errorType,
provider: provider.name,
});
// Create API error alert
setLlmErrorAlert({
type: 'error',
title,
description: errorInfo.message,
provider: provider.name,
errorType,
});
setData([]);
},
[provider.name, stop],
);
const clearApiErrorAlert = useCallback(() => {
setLlmErrorAlert(undefined);
}, []);
useEffect(() => { useEffect(() => {
const textarea = textareaRef.current; const textarea = textareaRef.current;
@@ -571,6 +640,8 @@ export const ChatImpl = memo(
clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()} clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()}
deployAlert={deployAlert} deployAlert={deployAlert}
clearDeployAlert={() => workbenchStore.clearDeployAlert()} clearDeployAlert={() => workbenchStore.clearDeployAlert()}
llmErrorAlert={llmErrorAlert}
clearLlmErrorAlert={clearApiErrorAlert}
data={chatData} data={chatData}
chatMode={chatMode} chatMode={chatMode}
setChatMode={setChatMode} setChatMode={setChatMode}

View File

@@ -0,0 +1,109 @@
import { AnimatePresence, motion } from 'framer-motion';
import type { LlmErrorAlertType } from '~/types/actions';
import { classNames } from '~/utils/classNames';
interface Props {
alert: LlmErrorAlertType;
clearAlert: () => void;
}
export default function LlmErrorAlert({ alert, clearAlert }: Props) {
const { title, description, provider, errorType } = alert;
const getErrorIcon = () => {
switch (errorType) {
case 'authentication':
return 'i-ph:key-duotone';
case 'rate_limit':
return 'i-ph:clock-duotone';
case 'quota':
return 'i-ph:warning-circle-duotone';
default:
return 'i-ph:warning-duotone';
}
};
const getErrorMessage = () => {
switch (errorType) {
case 'authentication':
return `Authentication failed with ${provider}. Please check your API key.`;
case 'rate_limit':
return `Rate limit exceeded for ${provider}. Please wait before retrying.`;
case 'quota':
return `Quota exceeded for ${provider}. Please check your account limits.`;
default:
return 'An error occurred while processing your request.';
}
};
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4 mb-2"
>
<div className="flex items-start">
<motion.div
className="flex-shrink-0"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2 }}
>
<div className={`${getErrorIcon()} text-xl text-bolt-elements-button-danger-text`}></div>
</motion.div>
<div className="ml-3 flex-1">
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-sm font-medium text-bolt-elements-textPrimary"
>
{title}
</motion.h3>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="mt-2 text-sm text-bolt-elements-textSecondary"
>
<p>{getErrorMessage()}</p>
{description && (
<div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
Error Details: {description}
</div>
)}
</motion.div>
<motion.div
className="mt-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex gap-2">
<button
onClick={clearAlert}
className={classNames(
'px-2 py-1.5 rounded-md text-sm font-medium',
'bg-bolt-elements-button-secondary-background',
'hover:bg-bolt-elements-button-secondary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
'text-bolt-elements-button-secondary-text',
)}
>
Dismiss
</button>
</div>
</motion.div>
</div>
</div>
</motion.div>
</AnimatePresence>
);
}

View File

@@ -361,16 +361,34 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
} catch (error: any) { } catch (error: any) {
logger.error(error); logger.error(error);
const errorResponse = {
error: true,
message: error.message || 'An unexpected error occurred',
statusCode: error.statusCode || 500,
isRetryable: error.isRetryable !== false, // Default to retryable unless explicitly false
provider: error.provider || 'unknown',
};
if (error.message?.includes('API key')) { if (error.message?.includes('API key')) {
throw new Response('Invalid or missing API key', { return new Response(
status: 401, JSON.stringify({
statusText: 'Unauthorized', ...errorResponse,
}); message: 'Invalid or missing API key',
statusCode: 401,
isRetryable: false,
}),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
statusText: 'Unauthorized',
},
);
} }
throw new Response(null, { return new Response(JSON.stringify(errorResponse), {
status: 500, status: errorResponse.statusCode,
statusText: 'Internal Server Error', headers: { 'Content-Type': 'application/json' },
statusText: 'Error',
}); });
} }
} }

View File

@@ -139,16 +139,34 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
} catch (error: unknown) { } catch (error: unknown) {
console.log(error); console.log(error);
const errorResponse = {
error: true,
message: error instanceof Error ? error.message : 'An unexpected error occurred',
statusCode: (error as any).statusCode || 500,
isRetryable: (error as any).isRetryable !== false,
provider: (error as any).provider || 'unknown',
};
if (error instanceof Error && error.message?.includes('API key')) { if (error instanceof Error && error.message?.includes('API key')) {
throw new Response('Invalid or missing API key', { return new Response(
status: 401, JSON.stringify({
statusText: 'Unauthorized', ...errorResponse,
}); message: 'Invalid or missing API key',
statusCode: 401,
isRetryable: false,
}),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
statusText: 'Unauthorized',
},
);
} }
throw new Response(null, { return new Response(JSON.stringify(errorResponse), {
status: 500, status: errorResponse.statusCode,
statusText: 'Internal Server Error', headers: { 'Content-Type': 'application/json' },
statusText: 'Error',
}); });
} }
} }

View File

@@ -62,6 +62,15 @@ export interface DeployAlert {
source?: 'vercel' | 'netlify' | 'github'; source?: 'vercel' | 'netlify' | 'github';
} }
export interface LlmErrorAlertType {
type: 'error' | 'warning';
title: string;
description: string;
content?: string;
provider?: string;
errorType?: 'authentication' | 'rate_limit' | 'quota' | 'network' | 'unknown';
}
export interface FileHistory { export interface FileHistory {
originalContent: string; originalContent: string;
lastModified: number; lastModified: number;