fix(gateway): surface actionable message for local model server connection errors
This commit is contained in:
+38
-1
@@ -407,6 +407,25 @@ _GATEWAY_RATE_LIMIT_RE = re.compile(
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_GATEWAY_CONNECTION_ERROR_RE = re.compile(
|
||||
r"("
|
||||
r"(?:\w+\.)?(?:api\s*)?connection\s*(?:error|timeout)"
|
||||
r"|(?:\w+\.)?connect\s*(?:error|timeout)"
|
||||
r"|connection\s+refused"
|
||||
r"|connection\s+reset"
|
||||
r"|connection\s+aborted"
|
||||
r"|actively\s+refused"
|
||||
r"|winerror\s+10061"
|
||||
r"|errno\s+111"
|
||||
r"|no\s+route\s+to\s+host"
|
||||
r"|network\s+is\s+unreachable"
|
||||
r"|cannot\s+connect"
|
||||
r"|failed\s+to\s+establish"
|
||||
r"|could\s+not\s+connect"
|
||||
r")",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_GATEWAY_SECRET_PATTERNS = (
|
||||
re.compile(r"\bsk-[A-Za-z0-9][A-Za-z0-9_\-]{12,}\b"),
|
||||
re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b"),
|
||||
@@ -665,7 +684,6 @@ def _format_exec_approval_fallback(
|
||||
+ ", ".join(choices[:-1]) + f", or {choices[-1]}."
|
||||
)
|
||||
|
||||
|
||||
def _gateway_provider_error_reply(text: str) -> str:
|
||||
"""Map raw provider/API errors to a short user-safe Telegram reply."""
|
||||
if _GATEWAY_AUTH_ERROR_RE.search(text):
|
||||
@@ -680,6 +698,11 @@ def _gateway_provider_error_reply(text: str) -> str:
|
||||
)
|
||||
if _GATEWAY_RATE_LIMIT_RE.search(text):
|
||||
return "⏱️ The model provider is rate-limiting requests. Please wait a moment and try again."
|
||||
if _GATEWAY_CONNECTION_ERROR_RE.search(text):
|
||||
return (
|
||||
"⚠️ The model server is not responding — it looks like the configured "
|
||||
"model endpoint is not running or is unreachable."
|
||||
)
|
||||
return (
|
||||
"⚠️ The model provider failed after retries. I kept raw provider details "
|
||||
"out of chat; check gateway logs for diagnostics."
|
||||
@@ -696,6 +719,20 @@ _GATEWAY_PROVIDER_ERROR_SHAPE_RE = re.compile(
|
||||
r"|http\s*\d{3}\b"
|
||||
r"|incorrect\s+api\s+key"
|
||||
r"|invalid\s+api\s+key"
|
||||
r"|(?:\w+\.)?(?:api\s*)?connection\s*(?:error|timeout)"
|
||||
r"|(?:\w+\.)?connect\s*(?:error|timeout)"
|
||||
r"|connection\s+refused"
|
||||
r"|connection\s+reset"
|
||||
r"|connection\s+aborted"
|
||||
r"|actively\s+refused"
|
||||
r"|winerror\s+10061"
|
||||
r"|errno\s+111"
|
||||
r"|no\s+route\s+to\s+host"
|
||||
r"|network\s+is\s+unreachable"
|
||||
r"|cannot\s+connect"
|
||||
r"|failed\s+to\s+establish"
|
||||
r"|could\s+not\s+connect"
|
||||
r"|all\s+connection\s+attempts\s+failed"
|
||||
r")",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Regression tests for #86570: gateway provider error connection messaging."""
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.run import (
|
||||
_GATEWAY_CONNECTION_ERROR_RE,
|
||||
_gateway_provider_error_reply,
|
||||
_looks_like_gateway_provider_error,
|
||||
)
|
||||
|
||||
|
||||
class TestGatewayConnectionErrorReply:
|
||||
def test_connection_error_strings_produce_specific_reply(self):
|
||||
samples = [
|
||||
"openai.APIConnectionError",
|
||||
"httpx.ConnectError: connection refused",
|
||||
"ConnectionError: [WinError 10061] No connection could be made",
|
||||
"Errno 111 Connection refused",
|
||||
"All connection attempts failed: Connection refused",
|
||||
"cannot connect to http://127.0.0.1:8033/v1",
|
||||
"failed to establish a new connection",
|
||||
]
|
||||
for text in samples:
|
||||
assert _looks_like_gateway_provider_error(text), text
|
||||
reply = _gateway_provider_error_reply(text)
|
||||
assert "not responding" in reply.lower(), text
|
||||
assert "not running or is unreachable" in reply, text
|
||||
|
||||
def test_other_errors_keep_generic_reply(self):
|
||||
for text in (
|
||||
"RuntimeError: model returned empty content",
|
||||
"Exception: unknown provider",
|
||||
"HTTP 500 internal server error",
|
||||
):
|
||||
if _looks_like_gateway_provider_error(text):
|
||||
reply = _gateway_provider_error_reply(text)
|
||||
assert "not running or is unreachable" not in reply, text
|
||||
|
||||
def test_connection_regex_does_not_match_non_connection_error(self):
|
||||
assert not _GATEWAY_CONNECTION_ERROR_RE.search("Rate limited after 3 retries")
|
||||
assert not _GATEWAY_CONNECTION_ERROR_RE.search("Provider authentication failed")
|
||||
|
||||
def test_auth_and_rate_limit_preserved(self):
|
||||
assert "authentication" in _gateway_provider_error_reply(
|
||||
"provider authentication failed"
|
||||
).lower()
|
||||
assert "rate-limiting" in _gateway_provider_error_reply(
|
||||
"rate limited after 3 retries"
|
||||
).lower()
|
||||
Reference in New Issue
Block a user