mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
refactor: use COPILOT_RETRYABLE_ERROR_PREFIX for server-driven retry
Replace frontend string matching on error text with a dedicated marker prefix. The backend now uses COPILOT_RETRYABLE_ERROR_PREFIX for transient errors, and the frontend checks markerType instead of matching "Anthropic connection interrupted". Also collapses remaining scattered ternaries and the base URL validation guard.
This commit is contained in:
@@ -4,6 +4,9 @@
|
||||
# The hex suffix makes accidental LLM generation of these strings virtually
|
||||
# impossible, avoiding false-positive marker detection in normal conversation.
|
||||
COPILOT_ERROR_PREFIX = "[__COPILOT_ERROR_f7a1__]" # Renders as ErrorCard
|
||||
COPILOT_RETRYABLE_ERROR_PREFIX = (
|
||||
"[__COPILOT_RETRYABLE_ERROR_a9c2__]" # ErrorCard + retry
|
||||
)
|
||||
COPILOT_SYSTEM_PREFIX = "[__COPILOT_SYSTEM_e3b0__]" # Renders as system info message
|
||||
|
||||
# Prefix for all synthetic IDs generated by CoPilot block execution.
|
||||
|
||||
@@ -39,6 +39,7 @@ from backend.util.settings import Settings
|
||||
from ..config import ChatConfig
|
||||
from ..constants import (
|
||||
COPILOT_ERROR_PREFIX,
|
||||
COPILOT_RETRYABLE_ERROR_PREFIX,
|
||||
COPILOT_SYSTEM_PREFIX,
|
||||
FRIENDLY_TRANSIENT_MSG,
|
||||
is_transient_api_error,
|
||||
@@ -231,18 +232,20 @@ def _build_sdk_env(
|
||||
}
|
||||
|
||||
# --- Mode 2: Direct Anthropic (no proxy hop) ---
|
||||
# Also the fallback when OpenRouter is enabled but credentials are missing
|
||||
# or the base URL is invalid.
|
||||
if not config.use_openrouter or not (config.api_key and config.base_url):
|
||||
# Also the fallback when OpenRouter is enabled but credentials are missing.
|
||||
# Strip /v1 suffix — SDK expects the base URL without a version path.
|
||||
base = (config.base_url or "").rstrip("/")
|
||||
if base.endswith("/v1"):
|
||||
base = base[:-3]
|
||||
if (
|
||||
not config.use_openrouter
|
||||
or not config.api_key
|
||||
or not base
|
||||
or not base.startswith("http")
|
||||
):
|
||||
return {}
|
||||
|
||||
# --- Mode 3: OpenRouter proxy ---
|
||||
base = config.base_url.rstrip("/")
|
||||
if base.endswith("/v1"):
|
||||
base = base[:-3]
|
||||
if not base or not base.startswith("http"):
|
||||
return {} # malformed base_url — fall back to direct
|
||||
|
||||
env: dict[str, str] = {
|
||||
"ANTHROPIC_BASE_URL": base,
|
||||
"ANTHROPIC_AUTH_TOKEN": config.api_key,
|
||||
@@ -1037,19 +1040,20 @@ async def stream_chat_completion_sdk(
|
||||
# Exception in receive_response() — capture it
|
||||
# so the session can still be saved and the
|
||||
# frontend gets a clean finish.
|
||||
err_str = str(stream_err)
|
||||
is_transient = is_transient_api_error(err_str)
|
||||
log = logger.warning if is_transient else logger.error
|
||||
display = (
|
||||
FRIENDLY_TRANSIENT_MSG
|
||||
if is_transient
|
||||
else f"SDK stream error: {stream_err}"
|
||||
)
|
||||
code = (
|
||||
"transient_api_error"
|
||||
if is_transient
|
||||
else "sdk_stream_error"
|
||||
)
|
||||
if is_transient_api_error(str(stream_err)):
|
||||
log, display, code, prefix = (
|
||||
logger.warning,
|
||||
FRIENDLY_TRANSIENT_MSG,
|
||||
"transient_api_error",
|
||||
COPILOT_RETRYABLE_ERROR_PREFIX,
|
||||
)
|
||||
else:
|
||||
log, display, code, prefix = (
|
||||
logger.error,
|
||||
f"SDK stream error: {stream_err}",
|
||||
"sdk_stream_error",
|
||||
COPILOT_ERROR_PREFIX,
|
||||
)
|
||||
|
||||
log(
|
||||
"%s Stream error from SDK: %s",
|
||||
@@ -1061,7 +1065,7 @@ async def stream_chat_completion_sdk(
|
||||
session.messages.append(
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
content=f"{COPILOT_ERROR_PREFIX} {display}",
|
||||
content=f"{prefix} {display}",
|
||||
)
|
||||
)
|
||||
yield StreamError(errorText=display, code=code)
|
||||
@@ -1111,7 +1115,7 @@ async def stream_chat_completion_sdk(
|
||||
session.messages.append(
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
content=f"{COPILOT_ERROR_PREFIX} {FRIENDLY_TRANSIENT_MSG}",
|
||||
content=f"{COPILOT_RETRYABLE_ERROR_PREFIX} {FRIENDLY_TRANSIENT_MSG}",
|
||||
)
|
||||
)
|
||||
yield StreamError(
|
||||
@@ -1226,10 +1230,15 @@ async def stream_chat_completion_sdk(
|
||||
response.errorText,
|
||||
response.code,
|
||||
)
|
||||
err_prefix = (
|
||||
COPILOT_RETRYABLE_ERROR_PREFIX
|
||||
if response.code == "transient_api_error"
|
||||
else COPILOT_ERROR_PREFIX
|
||||
)
|
||||
session.messages.append(
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
content=f"{COPILOT_ERROR_PREFIX} {response.errorText}",
|
||||
content=f"{err_prefix} {response.errorText}",
|
||||
)
|
||||
)
|
||||
ended_with_stream_error = True
|
||||
@@ -1427,8 +1436,14 @@ async def stream_chat_completion_sdk(
|
||||
else:
|
||||
logger.error("%s Error: %s", log_prefix, error_msg, exc_info=True)
|
||||
|
||||
is_transient = is_transient_api_error(error_msg)
|
||||
display_msg = FRIENDLY_TRANSIENT_MSG if is_transient else error_msg
|
||||
if is_transient_api_error(error_msg):
|
||||
display_msg, code, prefix = (
|
||||
FRIENDLY_TRANSIENT_MSG,
|
||||
"transient_api_error",
|
||||
COPILOT_RETRYABLE_ERROR_PREFIX,
|
||||
)
|
||||
else:
|
||||
display_msg, code, prefix = error_msg, "sdk_error", COPILOT_ERROR_PREFIX
|
||||
|
||||
# Append error marker to session (non-invasive text parsing approach)
|
||||
# The finally block will persist the session with this error marker
|
||||
@@ -1436,7 +1451,7 @@ async def stream_chat_completion_sdk(
|
||||
session.messages.append(
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
content=f"{COPILOT_ERROR_PREFIX} {display_msg}",
|
||||
content=f"{prefix} {display_msg}",
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
@@ -1450,10 +1465,7 @@ async def stream_chat_completion_sdk(
|
||||
isinstance(e, RuntimeError) and "cancel scope" in str(e)
|
||||
)
|
||||
if not is_cancellation:
|
||||
yield StreamError(
|
||||
errorText=display_msg,
|
||||
code="transient_api_error" if is_transient else "sdk_error",
|
||||
)
|
||||
yield StreamError(errorText=display_msg, code=code)
|
||||
|
||||
raise
|
||||
finally:
|
||||
|
||||
@@ -65,9 +65,6 @@ function WorkspaceMediaImage(props: React.JSX.IntrinsicElements["img"]) {
|
||||
/** Stable components override for Streamdown (avoids re-creating on every render). */
|
||||
const STREAMDOWN_COMPONENTS = { img: WorkspaceMediaImage };
|
||||
|
||||
/** Error text that the backend emits for transient Anthropic API failures. */
|
||||
const TRANSIENT_ERROR_TEXT = "Anthropic connection interrupted";
|
||||
|
||||
interface Props {
|
||||
part: UIMessage<unknown, UIDataTypes, UITools>["parts"][number];
|
||||
messageID: string;
|
||||
@@ -89,7 +86,7 @@ export function MessagePartRenderer({
|
||||
part.text,
|
||||
);
|
||||
|
||||
if (markerType === "error") {
|
||||
if (markerType === "error" || markerType === "retryable_error") {
|
||||
const lowerMarker = markerText.toLowerCase();
|
||||
const isCancellation =
|
||||
lowerMarker === "operation cancelled" ||
|
||||
@@ -104,15 +101,12 @@ export function MessagePartRenderer({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const isTransient = markerText
|
||||
.toLowerCase()
|
||||
.includes(TRANSIENT_ERROR_TEXT.toLowerCase());
|
||||
return (
|
||||
<ErrorCard
|
||||
key={key}
|
||||
responseError={{ message: markerText }}
|
||||
context="execution"
|
||||
onRetry={isTransient ? onRetry : undefined}
|
||||
onRetry={markerType === "retryable_error" ? onRetry : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,16 +172,22 @@ export function getTurnMessages(
|
||||
// The hex suffix makes it virtually impossible for an LLM to accidentally
|
||||
// produce these strings in normal conversation.
|
||||
const COPILOT_ERROR_PREFIX = "[__COPILOT_ERROR_f7a1__]";
|
||||
const COPILOT_RETRYABLE_ERROR_PREFIX = "[__COPILOT_RETRYABLE_ERROR_a9c2__]";
|
||||
const COPILOT_SYSTEM_PREFIX = "[__COPILOT_SYSTEM_e3b0__]";
|
||||
|
||||
export type MarkerType = "error" | "system" | null;
|
||||
export type MarkerType = "error" | "retryable_error" | "system" | null;
|
||||
|
||||
/** Escape all regex special characters in a string. */
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
// Pre-compiled marker regexes (avoids re-creating on every call / render)
|
||||
// Pre-compiled marker regexes (avoids re-creating on every call / render).
|
||||
// Retryable check must come first since it's more specific.
|
||||
const RETRYABLE_ERROR_MARKER_RE = new RegExp(
|
||||
`${escapeRegExp(COPILOT_RETRYABLE_ERROR_PREFIX)}\\s*(.+?)$`,
|
||||
"s",
|
||||
);
|
||||
const ERROR_MARKER_RE = new RegExp(
|
||||
`${escapeRegExp(COPILOT_ERROR_PREFIX)}\\s*(.+?)$`,
|
||||
"s",
|
||||
@@ -196,6 +202,15 @@ export function parseSpecialMarkers(text: string): {
|
||||
markerText: string;
|
||||
cleanText: string;
|
||||
} {
|
||||
const retryableMatch = text.match(RETRYABLE_ERROR_MARKER_RE);
|
||||
if (retryableMatch) {
|
||||
return {
|
||||
markerType: "retryable_error",
|
||||
markerText: retryableMatch[1].trim(),
|
||||
cleanText: text.replace(retryableMatch[0], "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
const errorMatch = text.match(ERROR_MARKER_RE);
|
||||
if (errorMatch) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user