Fixhancement: better LLM errors (#14031)

This commit is contained in:
shamoon
2026-09-09 16:02:43 +00:00
committed by GitHub
parent 7d67a10a35
commit bf716ebfd1
5 changed files with 121 additions and 3 deletions
+19 -3
View File
@@ -22,6 +22,7 @@ from paperless.network import validate_outbound_http_url
from paperless_ai.base_model import ClassificationSuggestions
from paperless_ai.base_model import DocumentClassifierSchema
from paperless_ai.base_model import model_to_classification_suggestions
from paperless_ai.exceptions import LLMProviderError
from paperless_ai.exceptions import LLMTimeoutError
logger = logging.getLogger("paperless_ai.client")
@@ -132,7 +133,7 @@ class AIClient:
from llama_index.core.llms import ChatMessage
if self.settings.llm_backend == LLMBackend.OLLAMA:
with self._normalize_timeouts():
with self._normalize_errors():
result = self.llm.chat(
[ChatMessage(role="user", content=prompt)],
format=DocumentClassifierSchema.model_json_schema(),
@@ -153,7 +154,7 @@ class AIClient:
content=f"{prompt}\n\n"
f"Answer by calling the {tool.metadata.name} tool. Do not write the answer as text.",
)
with self._normalize_timeouts():
with self._normalize_errors():
result = self.llm.chat_with_tools(
tools=[tool],
user_msg=user_msg,
@@ -173,7 +174,7 @@ class AIClient:
)
@contextmanager
def _normalize_timeouts(self) -> Iterator[None]:
def _normalize_errors(self) -> Iterator[None]:
try:
yield
except httpx.TimeoutException as exc:
@@ -181,8 +182,23 @@ class AIClient:
except Exception as exc:
if self._is_openai_timeout(exc):
raise LLMTimeoutError from exc
if self._is_provider_error(exc):
raise LLMProviderError from exc
raise
def _is_provider_error(self, exc: Exception) -> bool:
if self.settings.llm_backend == LLMBackend.OLLAMA:
from ollama import ResponseError
return isinstance(exc, ResponseError)
if self.settings.llm_backend == LLMBackend.OPENAI_LIKE:
from openai import APIStatusError
return isinstance(exc, APIStatusError)
return False
def _is_openai_timeout(self, exc: Exception) -> bool:
if self.settings.llm_backend != LLMBackend.OPENAI_LIKE:
return False
+4
View File
@@ -1,2 +1,6 @@
class LLMTimeoutError(Exception):
pass
class LLMProviderError(Exception):
"""The LLM backend rejected the request."""
+48
View File
@@ -4,6 +4,7 @@ from unittest.mock import MagicMock
from unittest.mock import patch
import httpx
import ollama
import openai
import pytest
from llama_index.core.llms.llm import ToolSelection
@@ -11,6 +12,7 @@ from llama_index.core.llms.llm import ToolSelection
from paperless_ai.client import LLM_SYSTEM_PROMPT
from paperless_ai.client import PLACEHOLDER_API_KEY
from paperless_ai.client import AIClient
from paperless_ai.exceptions import LLMProviderError
from paperless_ai.exceptions import LLMTimeoutError
@@ -214,6 +216,52 @@ def test_run_llm_query_openai_timeout_raises_local_error(
client.run_llm_query("test_prompt")
def test_run_llm_query_openai_status_error_raises_provider_error(
mock_ai_config,
mock_openai_llm,
):
mock_ai_config.llm_backend = "openai-like"
mock_ai_config.llm_model = "test_model"
mock_ai_config.llm_endpoint = "http://test-url"
request = httpx.Request("POST", "http://test-url/v1/chat/completions")
body = {"error": {"message": "Thinking mode does not support this tool_choice"}}
mock_openai_llm.return_value.chat_with_tools.side_effect = openai.BadRequestError(
"Error code: 400",
response=httpx.Response(400, request=request, json=body),
body=body,
)
client = AIClient()
with pytest.raises(LLMProviderError) as exc_info:
client.run_llm_query("test_prompt")
assert str(exc_info.value) == ""
assert isinstance(exc_info.value.__cause__, openai.BadRequestError)
def test_run_llm_query_ollama_response_error_raises_provider_error(
mock_ai_config,
mock_ollama_llm,
):
mock_ai_config.llm_backend = "ollama"
mock_ai_config.llm_model = "test_model"
mock_ai_config.llm_endpoint = "http://test-url"
response_error = ollama.ResponseError(
"confidential provider response",
status_code=400,
)
mock_ollama_llm.return_value.chat.side_effect = response_error
client = AIClient()
with pytest.raises(LLMProviderError) as exc_info:
client.run_llm_query("test_prompt")
assert str(exc_info.value) == ""
assert exc_info.value.__cause__ is response_error
def test_run_llm_query_httpx_timeout_raises_local_error(
mock_ai_config,
mock_ollama_llm,