feat(error-handling): implement LLM error adaptation and classification for chat streaming

- Introduced LLMErrorCategory and adapt_llm_exception to normalize LLM exceptions.
- Updated llm_retryable_message and llm_permanent_message to utilize the new adaptation logic.
- Enhanced classify_stream_exception to classify provider errors and return user-friendly messages.
- Added tests for error classification and adaptation to ensure robustness.
- Updated frontend error handling to display appropriate messages based on new classifications.
This commit is contained in:
Anish Sarkar 2026-06-12 05:03:14 +05:30
parent 203ef78346
commit 8e8cf96faa
9 changed files with 533 additions and 38 deletions

View file

@ -613,6 +613,18 @@ export default function NewChatPage() {
return;
}
if (normalized.channel === "inline") {
if (normalized.assistantMessage) {
await persistAssistantErrorMessage({
threadId,
assistantMsgId,
text: normalized.assistantMessage,
});
}
toast.error(normalized.userMessage);
return;
}
toast.error(normalized.userMessage);
},
[currentUser?.id, persistAssistantErrorMessage, searchSpaceId, setPremiumAlertForThread]

View file

@ -63,6 +63,21 @@ function normalizeFreeChatErrorMessage(error: unknown): string {
if (code === "THREAD_BUSY") {
return "A previous response is still stopping. Please try again in a moment.";
}
if (code === "MODEL_AUTH_FAILED") {
return "This models API key is invalid or expired. Switch models, or update the API key.";
}
if (code === "MODEL_NOT_FOUND") {
return "This model is unavailable or no longer exists. Please switch models.";
}
if (code === "MODEL_CONTEXT_LIMIT") {
return "This request is too large for the selected model. Reduce the input or switch models.";
}
if (code === "MODEL_PROVIDER_UNAVAILABLE") {
return "The selected model provider is temporarily unavailable. Please try again or switch models.";
}
if (code === "RATE_LIMITED") {
return "This model is temporarily rate-limited. Please try again in a few seconds or switch models.";
}
return error.message || "An unexpected error occurred";
}
@ -154,7 +169,7 @@ export function FreeChatPage() {
assistantMsgId: string,
signal: AbortSignal,
turnstileToken: string | null
): Promise<"captcha" | void> => {
): Promise<"captcha" | undefined> => {
const reqBody: Record<string, unknown> = {
model_slug: modelSlug,
messages: messageHistory,

View file

@ -5,6 +5,10 @@ export type ChatErrorKind =
| "thread_busy"
| "send_failed_pre_accept"
| "auth_expired"
| "model_auth_failed"
| "model_not_found"
| "model_context_limit"
| "model_provider_unavailable"
| "rate_limited"
| "network_offline"
| "stream_interrupted"
@ -14,7 +18,7 @@ export type ChatErrorKind =
| "server_error"
| "unknown";
export type ChatErrorChannel = "pinned_inline" | "toast" | "silent";
export type ChatErrorChannel = "pinned_inline" | "inline" | "toast" | "silent";
export type ChatTelemetryEvent = "chat_blocked" | "chat_error";
export type ChatErrorSeverity = "info" | "warn" | "error";
@ -206,6 +210,66 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError
};
}
if (errorCode === "MODEL_AUTH_FAILED") {
return {
kind: "model_auth_failed",
channel: "toast",
severity: "warn",
telemetryEvent: "chat_blocked",
isExpected: true,
userMessage:
"This models API key is invalid or expired. Switch models, or update the API key.",
rawMessage,
errorCode: errorCode ?? "MODEL_AUTH_FAILED",
details: { flow: input.flow, providerErrorType },
};
}
if (errorCode === "MODEL_NOT_FOUND") {
return {
kind: "model_not_found",
channel: "toast",
severity: "warn",
telemetryEvent: "chat_blocked",
isExpected: true,
userMessage:
"This model is unavailable or no longer exists. Switch to another model and try again.",
rawMessage,
errorCode: errorCode ?? "MODEL_NOT_FOUND",
details: { flow: input.flow, providerErrorType },
};
}
if (errorCode === "MODEL_CONTEXT_LIMIT") {
return {
kind: "model_context_limit",
channel: "toast",
severity: "warn",
telemetryEvent: "chat_blocked",
isExpected: true,
userMessage:
"This request is too large for the selected model. Reduce the input or switch models.",
rawMessage,
errorCode: errorCode ?? "MODEL_CONTEXT_LIMIT",
details: { flow: input.flow, providerErrorType },
};
}
if (errorCode === "MODEL_PROVIDER_UNAVAILABLE") {
return {
kind: "model_provider_unavailable",
channel: "toast",
severity: "warn",
telemetryEvent: "chat_blocked",
isExpected: true,
userMessage:
"The selected model provider is temporarily unavailable. Please try again or switch models.",
rawMessage,
errorCode: errorCode ?? "MODEL_PROVIDER_UNAVAILABLE",
details: { flow: input.flow, providerErrorType },
};
}
if (errorCode === "RATE_LIMITED" || providerTypeNormalized === "rate_limit_error") {
return {
kind: "rate_limited",

View file

@ -91,6 +91,10 @@ export function tagPreAcceptSendFailure(error: unknown): unknown {
"TURN_CANCELLING",
"AUTH_EXPIRED",
"UNAUTHORIZED",
"MODEL_AUTH_FAILED",
"MODEL_NOT_FOUND",
"MODEL_CONTEXT_LIMIT",
"MODEL_PROVIDER_UNAVAILABLE",
"RATE_LIMITED",
"NETWORK_ERROR",
"STREAM_PARSE_ERROR",