fix(errors): dispatch RateLimitError to RATE_LIMIT_ERROR, not MODEL_ERROR (#4086)

openai.RateLimitError subclasses APIError and was falling through to the
openai_unknown catch-all, which maps to MODEL_ERROR.  Since MODEL_ERROR
is checked before RATE_LIMIT_ERROR in categorize_error, a 429 was never
categorised as a rate-limit — users got the wrong error title, wrong
suggestions, and missed the API_QUOTA_WARNING notification.

Add an explicit RateLimitError branch in _dispatch() that produces an
openai_rate_limit token, map it to RATE_LIMIT_ERROR in ErrorReporter,
and add a Site C solution branch.
This commit is contained in:
LearningCircuit
2026-05-20 23:13:19 +02:00
committed by GitHub
parent cc02103625
commit 95f2e9b45a
4 changed files with 62 additions and 0 deletions

View File

@@ -76,6 +76,8 @@ class ErrorReporter:
r"API rate limit",
r"maximum.*requests.*minute",
r"maximum.*requests.*hour",
# OpenAI-compatible endpoint token (#3878 follow-up)
r"Error type: openai_rate_limit",
],
ErrorCategory.SEARCH_ERROR: [
r"Search.*failed",

View File

@@ -130,6 +130,16 @@ def _dispatch(
f"model currently loaded in {provider}.",
)
# Rate limit (429) -- must be checked before the APIError catch-all
# because RateLimitError subclasses APIStatusError -> APIError.
if _is("RateLimitError"):
return (
"openai_rate_limit",
f"{provider} at {base_url} rate-limited the request for model "
f"'{model}'. Wait a moment and retry, or enable LLM rate "
"limiting in Settings.",
)
# Bad request (400)
if _is("BadRequestError"):
return (

View File

@@ -1568,6 +1568,10 @@ def run_research_process(research_id, query, mode, **kwargs):
error_context = {
"solution": "Check the provider's logs for the full error and verify the base URL / model id."
}
elif "Error type: openai_rate_limit" in user_friendly_error:
error_context = {
"solution": "The provider rate-limited the request. Wait a moment and retry, or enable LLM Rate Limiting in Settings."
}
# Generate enhanced error report for failed research
enhanced_report_content = None

View File

@@ -17,6 +17,7 @@ from openai import (
BadRequestError,
NotFoundError,
PermissionDeniedError,
RateLimitError,
)
from local_deep_research.error_handling.error_reporter import (
@@ -229,6 +230,7 @@ class TestErrorReporterCategorisation:
("openai_model_not_found", ErrorCategory.MODEL_ERROR),
("openai_bad_request", ErrorCategory.MODEL_ERROR),
("openai_unknown", ErrorCategory.MODEL_ERROR),
("openai_rate_limit", ErrorCategory.RATE_LIMIT_ERROR),
],
)
def test_token_to_category(
@@ -433,3 +435,47 @@ class TestFriendlyErrorNoneArgs:
model=None,
)
assert "<unspecified>" in msg
# ---------------------------------------------------------------------------
# RateLimitError dispatch (follow-up to #3878)
# ---------------------------------------------------------------------------
class TestRateLimitErrorDispatch:
"""``openai.RateLimitError`` subclasses ``APIError`` via
``APIStatusError``. It must dispatch to ``openai_rate_limit`` (mapped to
RATE_LIMIT_ERROR) instead of falling through to the ``openai_unknown``
catch-all (mapped to MODEL_ERROR).
Without the explicit branch, a 429 is mis-categorised as MODEL_ERROR,
producing the wrong suggestions and skipping the API_QUOTA_WARNING
notification.
"""
def test_rate_limit_dispatches_to_rate_limit_token(self):
exc = RateLimitError(
message="Rate limit exceeded",
response=_resp(429),
body=None,
)
msg = friendly_openai_compatible_error(
exc,
provider="openrouter",
base_url="https://openrouter.ai/api/v1",
model="gpt-4o",
)
assert "Error type: openai_rate_limit" in msg
assert "Error type: openai_unknown" not in msg
assert "rate-limited" in msg
def test_rate_limit_categorised_as_rate_limit_error(self):
reporter = ErrorReporter()
message = (
"openrouter at https://openrouter.ai/api/v1 rate-limited the "
"request for model 'gpt-4o'. (Error type: openai_rate_limit) "
"| Details: Error code: 429"
)
assert (
reporter.categorize_error(message) == ErrorCategory.RATE_LIMIT_ERROR
)