Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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):
Expand All @@ -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."
Expand All @@ -696,6 +719,15 @@ 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"|all\s+connection\s+attempts\s+failed"
r")",
re.IGNORECASE,
)
Expand Down
63 changes: 63 additions & 0 deletions tests/gateway/test_local_model_connection_reply.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""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",
]
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_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",
"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()
Loading