From 55e729947b09b6e12121b965fe6addd3a51a4e62 Mon Sep 17 00:00:00 2001 From: Christopher <210261288+Christopher-Schulze@users.noreply.github.com> Date: Sat, 15 Aug 2026 07:27:43 +0200 Subject: [PATCH 1/2] fix(gateway): surface actionable message for local model server connection errors --- gateway/run.py | 39 ++++++++++++++- .../test_local_model_connection_reply.py | 49 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/gateway/test_local_model_connection_reply.py diff --git a/gateway/run.py b/gateway/run.py index 855f287667842..df24533b5ab6b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -407,6 +407,25 @@ def _gateway_surface_passes_raw_text(platform: Any) -> bool: 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 @@ def _gateway_provider_error_reply(text: str) -> str: 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, ) diff --git a/tests/gateway/test_local_model_connection_reply.py b/tests/gateway/test_local_model_connection_reply.py new file mode 100644 index 0000000000000..b82595c450e73 --- /dev/null +++ b/tests/gateway/test_local_model_connection_reply.py @@ -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() From d600150bf417cc12b385ce482880342f8b60e79c Mon Sep 17 00:00:00 2001 From: Christopher <210261288+Christopher-Schulze@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:28:40 +0200 Subject: [PATCH 2/2] fix(gateway): keep broad connection phrases out of the provider-error gate Fixes #86570 --- gateway/run.py | 5 ----- .../test_local_model_connection_reply.py | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index df24533b5ab6b..9eb1a411a9e64 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -727,11 +727,6 @@ def _gateway_provider_error_reply(text: str) -> str: 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, diff --git a/tests/gateway/test_local_model_connection_reply.py b/tests/gateway/test_local_model_connection_reply.py index b82595c450e73..ae52e318a71ca 100644 --- a/tests/gateway/test_local_model_connection_reply.py +++ b/tests/gateway/test_local_model_connection_reply.py @@ -17,8 +17,6 @@ def test_connection_error_strings_produce_specific_reply(self): "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 @@ -26,6 +24,22 @@ def test_connection_error_strings_produce_specific_reply(self): assert "not responding" in reply.lower(), text assert "not running or is unreachable" in reply, text + def test_broad_connection_phrases_still_map_once_classified(self): + """Reply selector keeps the full phrase set; the gate does not.""" + for text in ( + "cannot connect to http://127.0.0.1:8033/v1", + "failed to establish a new connection", + ): + reply = _gateway_provider_error_reply(text) + assert "not running or is unreachable" in reply, text + + def test_prose_cannot_connect_is_not_a_provider_error(self): + text = ( + "cannot connect to the office VPN from this cafe, " + "so I used the backup notes instead" + ) + assert not _looks_like_gateway_provider_error(text) + def test_other_errors_keep_generic_reply(self): for text in ( "RuntimeError: model returned empty content",