mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
## Summary - Detect transient Anthropic API errors (ECONNRESET, "socket connection was closed unexpectedly") across all error paths in the copilot SDK streaming loop - Replace raw technical error messages with user-friendly text: **"Anthropic connection interrupted — please retry"** - Add `retryable` field to `StreamError` model so the frontend can distinguish retryable errors - Add **"Try Again" button** on the error card for transient errors, which re-sends the last user message ### Background Sentry issue [AUTOGPT-SERVER-875](https://significant-gravitas.sentry.io/issues/AUTOGPT-SERVER-875) — 25+ events since March 13, caused by Anthropic API infrastructure instability (confirmed by their status page). Same SDK/code on dev and prod, prod-only because of higher volume of long-running streaming sessions. ### Changes **Backend (`constants.py`, `service.py`, `response_adapter.py`, `response_model.py`):** - `is_transient_api_error()` — pattern-matching helper for known transient error strings - Intercept transient errors in 3 places: `AssistantMessage.error`, stream exceptions, `BaseException` handler - Use friendly message in error markers persisted to session (so it shows properly on page refresh too) - `StreamError.retryable` field for frontend consumption **Frontend (`ChatContainer`, `ChatMessagesContainer`, `MessagePartRenderer`):** - Thread `onRetry` callback from `ChatContainer` → `ChatMessagesContainer` → `MessagePartRenderer` - Detect transient error text in error markers and show "Try Again" button via existing `ErrorCard.onRetry` - Clicking "Try Again" re-sends the last user message (backend auto-cleans stale error markers) Fixes SECRT-2128, SECRT-2129, SECRT-2130 ## Test plan - [ ] Verify transient error detection with `is_transient_api_error()` for known patterns - [ ] Confirm error card shows "Anthropic connection interrupted — please retry" instead of raw socket error - [ ] Confirm "Try Again" button appears on transient error cards - [ ] Confirm "Try Again" re-sends the last user message successfully - [ ] Confirm non-transient errors (e.g., "Prompt is too long") still show original error text without retry button - [ ] Verify error marker persists correctly on page refresh
90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
"""Unit tests for ChatConfig."""
|
|
|
|
import pytest
|
|
|
|
from .config import ChatConfig
|
|
|
|
# Env vars that the ChatConfig validators read — must be cleared so they don't
|
|
# override the explicit constructor values we pass in each test.
|
|
_ENV_VARS_TO_CLEAR = (
|
|
"CHAT_USE_E2B_SANDBOX",
|
|
"CHAT_E2B_API_KEY",
|
|
"E2B_API_KEY",
|
|
"CHAT_USE_OPENROUTER",
|
|
"CHAT_API_KEY",
|
|
"OPEN_ROUTER_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"CHAT_BASE_URL",
|
|
"OPENROUTER_BASE_URL",
|
|
"OPENAI_BASE_URL",
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
for var in _ENV_VARS_TO_CLEAR:
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
|
|
class TestOpenrouterActive:
|
|
"""Tests for the openrouter_active property."""
|
|
|
|
def test_enabled_with_credentials_returns_true(self):
|
|
cfg = ChatConfig(
|
|
use_openrouter=True,
|
|
api_key="or-key",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
)
|
|
assert cfg.openrouter_active is True
|
|
|
|
def test_enabled_but_missing_api_key_returns_false(self):
|
|
cfg = ChatConfig(
|
|
use_openrouter=True,
|
|
api_key=None,
|
|
base_url="https://openrouter.ai/api/v1",
|
|
)
|
|
assert cfg.openrouter_active is False
|
|
|
|
def test_disabled_returns_false_despite_credentials(self):
|
|
cfg = ChatConfig(
|
|
use_openrouter=False,
|
|
api_key="or-key",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
)
|
|
assert cfg.openrouter_active is False
|
|
|
|
def test_strips_v1_suffix_and_still_valid(self):
|
|
cfg = ChatConfig(
|
|
use_openrouter=True,
|
|
api_key="or-key",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
)
|
|
assert cfg.openrouter_active is True
|
|
|
|
def test_invalid_base_url_returns_false(self):
|
|
cfg = ChatConfig(
|
|
use_openrouter=True,
|
|
api_key="or-key",
|
|
base_url="not-a-url",
|
|
)
|
|
assert cfg.openrouter_active is False
|
|
|
|
|
|
class TestE2BActive:
|
|
"""Tests for the e2b_active property — single source of truth for E2B usage."""
|
|
|
|
def test_both_enabled_and_key_present_returns_true(self):
|
|
"""e2b_active is True when use_e2b_sandbox=True and e2b_api_key is set."""
|
|
cfg = ChatConfig(use_e2b_sandbox=True, e2b_api_key="test-key")
|
|
assert cfg.e2b_active is True
|
|
|
|
def test_enabled_but_missing_key_returns_false(self):
|
|
"""e2b_active is False when use_e2b_sandbox=True but e2b_api_key is absent."""
|
|
cfg = ChatConfig(use_e2b_sandbox=True, e2b_api_key=None)
|
|
assert cfg.e2b_active is False
|
|
|
|
def test_disabled_returns_false(self):
|
|
"""e2b_active is False when use_e2b_sandbox=False regardless of key."""
|
|
cfg = ChatConfig(use_e2b_sandbox=False, e2b_api_key="test-key")
|
|
assert cfg.e2b_active is False
|