-
Notifications
You must be signed in to change notification settings - Fork 19
✨(error) display specific error when LLM provider is down #495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # LLM Provider Error Handling | ||
|
|
||
| When the LLM provider fails, the backend emits a typed error code in the Vercel AI SDK stream. The frontend reads this code and displays a specific message. | ||
|
|
||
| ## Error mapping | ||
|
|
||
| | Provider failure | pydantic-ai exception | Error code | User-facing message | | ||
| |---|---|---|---| | ||
| | HTTP 5xx except 503 (500, 502, 504…) | `ModelHTTPError` (`status_code >= 500`) | `model_unavailable` | "The assistant is temporarily unavailable. Please try again later." | | ||
| | HTTP 503 (service busy) | `ModelHTTPError` (`status_code == 503`) | `model_busy` | "The assistant is too busy. Please try again later." | | ||
| | HTTP 429 (rate limit) | `ModelHTTPError` (`status_code == 429`) | `model_rate_limited` | "The assistant is overloaded. Please try again in a few minutes." | | ||
| | HTTP 404 (not found) | `ModelHTTPError` (`status_code == 404`) | `model_not_found` | "Model not found." | | ||
| | HTTP 422 (validation error) | `HTTPValidationError` (`status_code == 422`) | `model_wrong_type` | "Wrong model type." | | ||
| | No TCP connection, DNS failure, timeout | `ModelAPIError` | `model_connection_error` | "Unable to reach the assistant. Please check your connection or try again later." | | ||
| | Any other error | uncaught → generic Vercel AI SDK error | `generic` | "Sorry, an error occurred. Please try again." | | ||
|
|
||
| ## Status page link | ||
|
|
||
| Set `STATUS_PAGE_URL` to display a "Check service status" link alongside any non-`generic` provider error (`model_unavailable`, `model_busy`, `model_rate_limited`, `model_not_found`, `model_wrong_type`, `model_connection_error`). | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ```bash | ||
| STATUS_PAGE_URL=https://albert.sites.beta.gouv.fr/about/status/ | ||
| ``` | ||
|
|
||
| The link is omitted when the variable is unset. | ||
|
|
||
| ## Implementation | ||
|
|
||
| - **Backend:** `src/backend/chat/clients/pydantic_ai.py` — `_stream_content` catches provider exceptions and emits `ErrorPart` events. | ||
| - **Frontend:** `src/frontend/apps/conversations/src/features/chat/components/ChatError.tsx` — renders the message based on `errorType` prop. | ||
| - **Config:** `src/backend/conversations/settings.py` and `src/backend/core/api/viewsets.py` — `STATUS_PAGE_URL` is exposed via the `config/` API. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,7 +97,9 @@ | |
|
|
||
| from asgiref.sync import sync_to_async | ||
| from langfuse import get_client | ||
| from mistralai.client.errors import HTTPValidationError, SDKError | ||
| from pydantic_ai import Agent, InstrumentationSettings, RunContext, RunUsage | ||
| from pydantic_ai.exceptions import ModelAPIError, ModelHTTPError | ||
| from pydantic_ai.messages import ( | ||
| BinaryContent, | ||
| DocumentUrl, | ||
|
|
@@ -177,6 +179,22 @@ | |
| CACHE_TIMEOUT = 30 * 60 # 30 minutes timeout | ||
| DOCUMENT_URL_PREFIX = "/media-key/" | ||
|
|
||
| _HTTP_STATUS_TO_ERROR_CODE = { | ||
| 429: "model_rate_limited", | ||
| 503: "model_busy", | ||
| 404: "model_not_found", | ||
| 422: "model_wrong_type", | ||
| } | ||
|
|
||
|
|
||
| def _resolve_http_error_code(status_code: int) -> str | None: | ||
| """Map an HTTP status code to an error code string, or None to re-raise.""" | ||
| if status_code in _HTTP_STATUS_TO_ERROR_CODE: | ||
| return _HTTP_STATUS_TO_ERROR_CODE[status_code] | ||
| if status_code >= 500: | ||
| return "model_unavailable" | ||
| return None | ||
|
|
||
|
|
||
| def get_model_configuration(model_hrid: str): | ||
| """Get the model configuration from settings.""" | ||
|
|
@@ -342,9 +360,38 @@ async def _stream_content( | |
| span = stack.enter_context(get_client().start_as_current_span(name="conversation")) | ||
| span.update_trace(user_id=str(self.user.sub), session_id=str(self.conversation.pk)) | ||
|
|
||
| async for event in self._run_agent(messages, force_web_search): | ||
| if stream_text := encoder_fn(event): | ||
| yield stream_text | ||
| try: | ||
| async for event in self._run_agent(messages, force_web_search): | ||
| if stream_text := encoder_fn(event): | ||
| yield stream_text | ||
| except (ModelHTTPError, HTTPValidationError, SDKError) as exc: | ||
| # HTTPValidationError and SDKError are mistral-specific exceptions not | ||
| # wrapped by pydantic_ai into ModelHTTPError. | ||
| error_code = _resolve_http_error_code(exc.status_code) | ||
| if error_code is None: | ||
|
Comment on lines
+367
to
+371
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard Line 370 assumes every caught exception has 💡 Minimal hardening patch- except (ModelHTTPError, HTTPValidationError, SDKError) as exc:
+ except (ModelHTTPError, HTTPValidationError, SDKError) as exc:
# HTTPValidationError and SDKError are mistral-specific exceptions not
# wrapped by pydantic_ai into ModelHTTPError.
- error_code = _resolve_http_error_code(exc.status_code)
+ status_code = getattr(exc, "status_code", None)
+ if status_code is None:
+ error_code = "model_connection_error"
+ else:
+ error_code = _resolve_http_error_code(status_code)
if error_code is None:
raise
logger.exception(
"LLM provider HTTP error (status=%s) for conversation %s: %s",
- exc.status_code,
+ status_code,
self.conversation.pk,
exc,
)🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SDKError and HTTPValidationError inherit from MistralError There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
(ᵕ—ᴗ—) 🐇 🧠 Learnings used |
||
| raise | ||
| logger.exception( | ||
| "LLM provider HTTP error (status=%s) for conversation %s: %s", | ||
| exc.status_code, | ||
| self.conversation.pk, | ||
| exc, | ||
| ) | ||
| error_event = events_v4.ErrorPart(error=error_code) | ||
| if encoded := encoder_fn(error_event): | ||
| yield encoded | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| else: | ||
| raise | ||
| except ModelAPIError as exc: | ||
| logger.exception( | ||
| "LLM provider connection error for conversation %s: %s", | ||
| self.conversation.pk, | ||
| exc, | ||
| ) | ||
| error_event = events_v4.ErrorPart(error="model_connection_error") | ||
| if encoded := encoder_fn(error_event): | ||
| yield encoded | ||
| else: | ||
| raise | ||
|
|
||
| async def stream_text_async(self, messages: List[UIMessage], force_web_search: bool = False): | ||
| """Return only the assistant text deltas (legacy text mode).""" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.