Unverified 提交 69a731c3 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Fix error message handling in local agent error responses (#2983)

## Summary - Improve error message handling in local agent error responses by using `getErrorMessage()` helper - This ensures consistent error message formatting across the application ## Test plan - Verify local agent error responses display proper error messages - Check that the error handling works for various error types 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2983" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
上级 9205717e
{ {
"name": "dyad", "name": "dyad",
"version": "0.39.0-beta.1", "version": "0.39.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dyad", "name": "dyad",
"version": "0.39.0-beta.1", "version": "0.39.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.46", "@ai-sdk/amazon-bedrock": "^4.0.46",
......
...@@ -4,6 +4,9 @@ import type { ...@@ -4,6 +4,9 @@ import type {
LanguageModelV3StreamPart, LanguageModelV3StreamPart,
} from "@ai-sdk/provider"; } from "@ai-sdk/provider";
import type { LanguageModel } from "ai"; import type { LanguageModel } from "ai";
import log from "electron-log";
const logger = log.scope("fallback_model");
// Types // Types
interface FallbackSettings { interface FallbackSettings {
...@@ -55,15 +58,24 @@ const RETRYABLE_ERROR_PATTERNS = [ ...@@ -55,15 +58,24 @@ const RETRYABLE_ERROR_PATTERNS = [
"econnreset", "econnreset",
"epipe", "epipe",
"etimedout", "etimedout",
"unknown_error",
]; ];
export function defaultShouldRetryThisError(error: any): boolean { export function defaultShouldRetryThisError(error: any): boolean {
if (!error) return false; if (!error) return false;
try { try {
// Check status code // Some API errors nest the real details inside an `error` property
// (e.g. { type: 'error', error: { type, code, message } }).
const inner = error?.error;
// Check status code on the error or its inner wrapper
const statusCode = const statusCode =
error?.statusCode || error?.status || error?.response?.status; error?.statusCode ||
error?.status ||
error?.response?.status ||
inner?.statusCode ||
inner?.status;
if ( if (
statusCode && statusCode &&
(RETRYABLE_STATUS_CODES.has(statusCode) || statusCode >= 500) (RETRYABLE_STATUS_CODES.has(statusCode) || statusCode >= 500)
...@@ -71,17 +83,28 @@ export function defaultShouldRetryThisError(error: any): boolean { ...@@ -71,17 +83,28 @@ export function defaultShouldRetryThisError(error: any): boolean {
return true; return true;
} }
// Check error message patterns // Concatenate fields from both the outer error and the inner wrapper
const errorString = ( // so we don't miss nested codes/types.
error?.message || const errorString =
error?.code || [
error?.type || error?.message,
JSON.stringify(error) error?.code,
).toLowerCase(); error?.type,
inner?.message,
return RETRYABLE_ERROR_PATTERNS.some((pattern) => inner?.code,
inner?.type,
]
.filter(Boolean)
.join(" ")
.toLowerCase() || JSON.stringify(error).toLowerCase();
const isRetryable = RETRYABLE_ERROR_PATTERNS.some((pattern) =>
errorString.includes(pattern), errorString.includes(pattern),
); );
logger.info(
`Error retryable=${isRetryable}, statusCode=${statusCode ?? "none"}, errorString="${errorString.slice(0, 200)}"`,
);
return isRetryable;
} catch { } catch {
// If we can't parse the error, don't retry // If we can't parse the error, don't retry
return false; return false;
...@@ -188,15 +211,18 @@ class FallbackModel implements LanguageModelV3 { ...@@ -188,15 +211,18 @@ class FallbackModel implements LanguageModelV3 {
// Check if we should retry this error // Check if we should retry this error
if (!defaultShouldRetryThisError(err)) { if (!defaultShouldRetryThisError(err)) {
logger.warn(
`Non-retryable error from model ${this.modelId}, not falling back`,
);
throw err; throw err;
} }
// Call error handler if provided
// If we've tried all models at least once and still failing, throw // If we've tried all models at least once and still failing, throw
if (state.modelsAttempted.size === this.settings.models.length) { if (state.modelsAttempted.size === this.settings.models.length) {
// If we haven't hit max retries yet, we can try models again
if (state.attemptNumber >= this.maxRetries) { if (state.attemptNumber >= this.maxRetries) {
logger.error(
`All ${this.settings.models.length} models exhausted for ${operationName} after ${state.attemptNumber} attempts`,
);
throw new Error( throw new Error(
`All ${this.settings.models.length} models failed for ${operationName}. ` + `All ${this.settings.models.length} models failed for ${operationName}. ` +
`Last error: ${err.message}`, `Last error: ${err.message}`,
...@@ -207,6 +233,9 @@ class FallbackModel implements LanguageModelV3 { ...@@ -207,6 +233,9 @@ class FallbackModel implements LanguageModelV3 {
// Switch to next model // Switch to next model
this.switchToNextModel(); this.switchToNextModel();
state.modelsAttempted.add(this.currentModelIndex); state.modelsAttempted.add(this.currentModelIndex);
logger.info(
`Falling back to model ${this.modelId} (attempt ${state.attemptNumber}/${this.maxRetries})`,
);
} }
} }
...@@ -320,6 +349,9 @@ class FallbackModel implements LanguageModelV3 { ...@@ -320,6 +349,9 @@ class FallbackModel implements LanguageModelV3 {
fallbackModel.settings.models.length && fallbackModel.settings.models.length &&
retryState.attemptNumber >= fallbackModel.maxRetries retryState.attemptNumber >= fallbackModel.maxRetries
) { ) {
logger.error(
`All models exhausted during streaming after ${retryState.attemptNumber} attempts`,
);
controller.error( controller.error(
new Error( new Error(
`All models failed during streaming. Last error: ${err.message}`, `All models failed during streaming. Last error: ${err.message}`,
...@@ -328,18 +360,22 @@ class FallbackModel implements LanguageModelV3 { ...@@ -328,18 +360,22 @@ class FallbackModel implements LanguageModelV3 {
return; return;
} }
logger.info(
`Stream error from model, falling back to ${fallbackModel.modelId} (attempt ${retryState.attemptNumber}/${fallbackModel.maxRetries})`,
);
try { try {
// Create a new stream with the next model
const nextResult = await fallbackModel const nextResult = await fallbackModel
.getUnderlyingModel() .getUnderlyingModel()
.doStream(options); .doStream(options);
await processStream(nextResult.stream); await processStream(nextResult.stream);
} catch (nextError) { } catch (nextError) {
// If the retry also fails, propagate the error
controller.error(nextError); controller.error(nextError);
} }
} else { } else {
// Don't retry - propagate the error logger.warn(
`Stream error not retryable (hasContent=${hasStreamedContent}, retryable=${defaultShouldRetryThisError(err)}, attempts=${retryState.attemptNumber}/${fallbackModel.maxRetries}), propagating error`,
);
controller.error(err); controller.error(err);
} }
} }
......
...@@ -1266,7 +1266,7 @@ export async function handleLocalAgentStream( ...@@ -1266,7 +1266,7 @@ export async function handleLocalAgentStream(
logger.error("Local agent error:", error); logger.error("Local agent error:", error);
safeSend(event.sender, "chat:response:error", { safeSend(event.sender, "chat:response:error", {
chatId: req.chatId, chatId: req.chatId,
error: `Error: ${error}`, error: `Error: ${getErrorMessage(error)}`,
}); });
return false; // Error - don't consume quota return false; // Error - don't consume quota
} }
...@@ -1293,6 +1293,17 @@ function getErrorMessage(error: unknown): string { ...@@ -1293,6 +1293,17 @@ function getErrorMessage(error: unknown): string {
if (typeof error === "string") { if (typeof error === "string") {
return error; return error;
} }
if (isRecord(error)) {
if (typeof error.message === "string" && error.message.length > 0) {
return error.message;
}
if ("error" in error) {
return getErrorMessage(error.error);
}
if ("cause" in error) {
return getErrorMessage(error.cause);
}
}
try { try {
return JSON.stringify(error); return JSON.stringify(error);
} catch { } catch {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论