fix: explain provider DNS failures as possible offline state

This commit is contained in:
Teknium 2026-08-08 14:03:45 -07:00
parent bdfdd2773f
commit 5f4a7e99f0
2 changed files with 63 additions and 3 deletions

View File

@ -2529,11 +2529,38 @@ class AIAgent:
"""Extract a human-readable one-liner from an API error.
Handles Cloudflare HTML error pages (502, 503, etc.) by pulling the
<title> tag instead of dumping raw HTML. Falls back to a truncated
str(error) for everything else.
<title> tag instead of dumping raw HTML. Network/DNS failures are
translated into an offline hint, including when an SDK wraps the
original OS error. Falls back to a truncated str(error) otherwise.
"""
raw = str(error)
# Linux, macOS, and Windows use different low-level messages when DNS
# cannot resolve the provider while the device is offline. SDKs often
# wrap that OSError in a generic "Connection error", so inspect the
# exception chain before showing the top-level message to the user.
network_resolution_markers = (
"temporary failure in name resolution",
"name or service not known",
"nodename nor servname provided, or not known",
"getaddrinfo failed",
"no address associated with hostname",
"network is unreachable",
)
current: Optional[BaseException] = error
seen: set[int] = set()
while current is not None and id(current) not in seen:
seen.add(id(current))
if any(
marker in str(current).lower()
for marker in network_resolution_markers
):
return (
"Hermes can't reach the model provider. You may be offline. "
"Check your internet connection and try again."
)
current = current.__cause__ or current.__context__
if (
isinstance(error, ValueError)
and "expected ident at line" in raw.lower()

View File

@ -14,6 +14,7 @@ from types import SimpleNamespace
from typing import Any
import httpx
import pytest
from run_agent import AIAgent
@ -37,9 +38,42 @@ def test_empty_body_falls_back_to_response_json_error_message():
assert "HTTP 400" in summary
assert "model `foo` does not exist" in summary
@pytest.mark.parametrize(
"technical_message",
[
"Temporary failure in name resolution",
"Name or service not known",
"nodename nor servname provided, or not known",
"getaddrinfo failed",
"No address associated with hostname",
"Network is unreachable",
],
)
def test_network_resolution_failure_explains_that_the_user_may_be_offline(
technical_message,
):
error = OSError(-3, technical_message)
summary = AIAgent._summarize_api_error(error)
assert summary == (
"Hermes can't reach the model provider. You may be offline. "
"Check your internet connection and try again."
)
assert "name resolution" not in summary.lower()
def test_wrapped_dns_resolution_failure_gets_the_same_friendly_message():
try:
try:
raise OSError(-3, "Temporary failure in name resolution")
except OSError as cause:
raise RuntimeError("Connection error.") from cause
except RuntimeError as error:
summary = AIAgent._summarize_api_error(error)
assert "You may be offline" in summary
assert "Connection error" not in summary
def test_unread_streaming_response_does_not_crash_and_falls_back_to_exception_message():
@ -62,4 +96,3 @@ def test_unread_streaming_response_does_not_crash_and_falls_back_to_exception_me
summary = AIAgent._summarize_api_error(err)
assert "HTTP 429" in summary
assert "Gemini HTTP 429: quota exceeded" in summary