diff --git a/ai-hub/app/core/orchestration/architect.py b/ai-hub/app/core/orchestration/architect.py index e219cb9..e6d5a2c 100644 --- a/ai-hub/app/core/orchestration/architect.py +++ b/ai-hub/app/core/orchestration/architect.py @@ -1,14 +1,38 @@ import logging import queue import time +import traceback from typing import List, Dict, Any, Optional +import litellm + from app.db import models from .memory import ContextManager from .stream import StreamProcessor from .body import ToolExecutor from .guards import SafetyGuard +_PROVIDER_ERROR_TYPES = ( + litellm.ServiceUnavailableError, + litellm.InternalServerError, + litellm.RateLimitError, + litellm.APIConnectionError, + litellm.APIError, + litellm.Timeout, + litellm.AuthenticationError, +) + +def _is_provider_error(e: Exception) -> bool: + """True for transient or user-actionable provider errors — not code bugs.""" + if isinstance(e, _PROVIDER_ERROR_TYPES): + return True + # MidStreamFallbackError wraps another exception — match by name to avoid version drift + if type(e).__name__ in ("MidStreamFallbackError", "ContextWindowExceededError"): + return True + msg = str(e).lower() + return any(tok in msg for tok in ("503", "502", "429", "unavailable", "rate limit", "timeout", "overloaded")) + + class Architect: """ The Master-Architect Orchestrator. @@ -113,10 +137,20 @@ yield {"type": "status", "content": f"Task complete in {elapsed:.1f}s"} except Exception as e: - import traceback - logging.error(f"[Architect] CRITICAL FAULT:\n{traceback.format_exc()}") - yield {"type": "status", "content": "Fatal Orchestration Error"} - yield {"type": "content", "content": f"\n\n> **🚨 Core Orchestrator Fault:** `{str(e)}`"} + if _is_provider_error(e): + logging.warning(f"[Architect] Provider error (non-fatal): {e}") + model_hint = getattr(llm_provider, 'model_name', 'the AI model') + yield {"type": "status", "content": "AI provider temporarily unavailable"} + yield {"type": "content", "content": ( + f"\n\n> ⚠️ **`{model_hint}` is temporarily unavailable.**\n" + f">\n" + f"> The provider returned a service error (likely overloaded or a preview endpoint outage).\n" + f"> **Please try your message again** — or go to **Settings → AI Provider** to switch to a stable model." + )} + else: + logging.error(f"[Architect] CRITICAL FAULT:\n{traceback.format_exc()}") + yield {"type": "status", "content": "Fatal Orchestration Error"} + yield {"type": "content", "content": f"\n\n> **🚨 Core Orchestrator Fault:** `{str(e)}`"} finally: if registry and user_id: registry.unsubscribe_user(user_id, mesh_bridge) # --- M7: Automatic Terminal Task Cancellation --- @@ -219,9 +253,13 @@ if tools: kwargs["tools"] = tools kwargs["tool_choice"] = "auto" + # Let provider errors propagate so the top-level handler can show a user-friendly message. + # Only swallow true unknowns (e.g. missing provider config) which return None to exit cleanly. try: return await llm_provider.acompletion(messages=messages, timeout=60, **kwargs) except Exception as e: + if _is_provider_error(e): + raise logging.error(f"[Architect] LLM Exception: {e}") return None diff --git a/ai-hub/app/core/providers/llm/general.py b/ai-hub/app/core/providers/llm/general.py index 906510b..4b1329a 100644 --- a/ai-hub/app/core/providers/llm/general.py +++ b/ai-hub/app/core/providers/llm/general.py @@ -60,10 +60,17 @@ } try: return await self._acompletion_with_retry(request) + except litellm.AuthenticationError as e: + raise litellm.AuthenticationError( + f"API key rejected by {self.model_name}. Please update your API key in Settings → AI Provider.", + model=self.model_name, llm_provider="" + ) from e except Exception as e: err_msg = str(e) if "authentication" in err_msg.lower() or "401" in err_msg: - raise RuntimeError(f"Authentication failed for {self.model_name}. Check your API key.") - - # If we still fail after retries, wrap it in a cleaner runtime error - raise RuntimeError(f"Core Orchestrator Fault: {err_msg}") \ No newline at end of file + raise litellm.AuthenticationError( + f"Authentication failed for {self.model_name}. Check your API key in Settings → AI Provider.", + model=self.model_name, llm_provider="" + ) from e + # Re-raise with original type preserved so callers can classify provider vs code errors + raise \ No newline at end of file