From 156a01627942e6e0106028feb5091f2a4ea4a88d Mon Sep 17 00:00:00 2001 From: Pranav Agarkar <90404176+PranavAgarkar07@users.noreply.github.com> Date: Wed, 13 May 2026 10:06:39 +0530 Subject: [PATCH] fix(chat): show specific error messages instead of generic 'Something went wrong' (#1566) Co-authored-by: Steven Enamakel --- app/src/providers/ChatRuntimeProvider.tsx | 19 ++++--- app/src/services/chatService.ts | 13 ++++- src/openhuman/channels/providers/web.rs | 63 +++++++++++++++++++++-- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/app/src/providers/ChatRuntimeProvider.tsx b/app/src/providers/ChatRuntimeProvider.tsx index c71120787..a627894a3 100644 --- a/app/src/providers/ChatRuntimeProvider.tsx +++ b/app/src/providers/ChatRuntimeProvider.tsx @@ -809,7 +809,7 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => { finishChatDoneTurn(event, 'ordinary'); }, onError: event => { - const eventKey = `error:${event.thread_id}:${event.request_id ?? 'none'}:${event.error_type}:${event.message}`; + const eventKey = `error:${event.thread_id}:${event.request_id ?? 'none'}:${event.error_type}`; if ( !markChatEventSeen(eventKey, { threadId: event.thread_id, requestId: event.request_id }) ) @@ -840,14 +840,17 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => { const currentState = store.getState(); const threadMessages = currentState.thread.messagesByThreadId[event.thread_id] ?? []; const lastMsg = threadMessages[threadMessages.length - 1]; - if ( - !(lastMsg?.sender === 'agent' && lastMsg?.content === USER_FACING_AGENT_ERROR_MESSAGE) - ) { + // For the generic 'inference' type the server may send a raw internal error string; + // use the safe user-facing constant instead. For all other classified types + // (rate_limited, timeout, auth_error, etc.) the message comes from + // classify_inference_error() in web.rs and is already user-friendly. + const errorContent = + event.error_type === 'inference' + ? USER_FACING_AGENT_ERROR_MESSAGE + : event.message || USER_FACING_AGENT_ERROR_MESSAGE; + if (!(lastMsg?.sender === 'agent' && lastMsg?.content === errorContent)) { void dispatch( - addInferenceResponse({ - content: USER_FACING_AGENT_ERROR_MESSAGE, - threadId: event.thread_id, - }) + addInferenceResponse({ content: errorContent, threadId: event.thread_id }) ); } diff --git a/app/src/services/chatService.ts b/app/src/services/chatService.ts index 9ca58dc1a..0ec9ff312 100644 --- a/app/src/services/chatService.ts +++ b/app/src/services/chatService.ts @@ -90,7 +90,18 @@ export interface ChatErrorEvent { thread_id: string; request_id?: string; message: string; - error_type: 'network' | 'timeout' | 'tool_error' | 'inference' | 'cancelled'; + error_type: + | 'network' + | 'timeout' + | 'tool_error' + | 'inference' + | 'cancelled' + | 'rate_limited' + | 'auth_error' + | 'provider_error' + | 'context_overflow' + | 'model_unavailable' + | 'budget_exhausted'; round: number | null; } diff --git a/src/openhuman/channels/providers/web.rs b/src/openhuman/channels/providers/web.rs index 8d49dbab1..50889b578 100644 --- a/src/openhuman/channels/providers/web.rs +++ b/src/openhuman/channels/providers/web.rs @@ -130,6 +130,62 @@ fn generic_inference_error_user_message() -> &'static str { "Something went wrong. Please try again.\nThis error has been reported. You can also report it on Discord.\nReport on Discord" } +fn classify_inference_error(err: &str) -> (&'static str, &'static str) { + let lower = err.to_lowercase(); + if lower.contains("rate limit") || lower.contains("429") { + ( + "rate_limited", + "You're being rate-limited. Please wait a moment and try again.", + ) + } else if lower.contains("timeout") || lower.contains("timed out") { + ( + "timeout", + "The request timed out. Please check your connection and try again.", + ) + } else if lower.contains("401") || lower.contains("unauthorized") || lower.contains("api key") { + ( + "auth_error", + "There's an authentication issue with the AI provider. Please check your API key in settings.", + ) + } else if lower.contains("402") + || lower.contains("payment required") + || lower.contains("insufficient balance") + { + ( + "budget_exhausted", + "Insufficient credits. Please top up to continue.", + ) + } else if lower.contains("500") + || lower.contains("internal server") + || lower.contains("service unavailable") + || lower.contains("503") + { + ( + "provider_error", + "The AI provider is temporarily unavailable. Please try again later.", + ) + } else if lower.contains("context") + && (lower.contains("length") + || lower.contains("limit") + || lower.contains("exceed") + || lower.contains("token")) + { + ( + "context_overflow", + "The conversation is too long. Please start a new chat.", + ) + } else if lower.contains("model") + && (lower.contains("not found") || lower.contains("unavailable")) + { + ( + "model_unavailable", + "The selected model is not available. Please check your model settings.", + ) + } else { + ("inference", generic_inference_error_user_message()) + } +} + fn prompt_guard_user_message(action: PromptEnforcementAction) -> &'static str { match action { PromptEnforcementAction::Allow => "Message accepted.", @@ -280,13 +336,14 @@ pub async fn start_chat( "run_chat_task failed client_id={} thread_id={} request_id={} error={}", client_id_task, thread_id_task, request_id_task, err ); + let (classified_type, classified_message) = classify_inference_error(&err); crate::core::observability::report_error( detailed.as_str(), "web_channel", "run_chat_task", &[ ("channel", "web"), - ("error_type", "inference"), + ("error_type", classified_type), ("thread_id", thread_id_task.as_str()), ("request_id", request_id_task.as_str()), ], @@ -297,8 +354,8 @@ pub async fn start_chat( thread_id: thread_id_task.clone(), request_id: request_id_task.clone(), full_response: None, - message: Some(generic_inference_error_user_message().to_string()), - error_type: Some("inference".to_string()), + message: Some(classified_message.to_string()), + error_type: Some(classified_type.to_string()), tool_name: None, skill_id: None, args: None,