fix(model_metadata): widen localhost->IPv4 rewrite to all sibling probe sites

#37595 fixed the Windows dual-stack IPv6 timeout only inside
detect_local_server_type. The same 2s-per-probe penalty existed at every
other helper that builds a probe URL from base_url. Extract the rewrite
into _localhost_to_ipv4() and apply it at:

- query_ollama_num_ctx
- query_ollama_supports_vision
- _query_ollama_api_show (server_url derivation)
- _query_local_context_length (server root + LM Studio native URL)

Tests cover the helper's URL forms, non-localhost passthrough, and that
the ollama probes actually POST to 127.0.0.1.
This commit is contained in:
Kshitij Kapoor 2026-07-09 13:32:51 +05:30 committed by kshitij
parent 040e30aa72
commit 20cb385328
3 changed files with 60 additions and 10 deletions

View file

@ -636,6 +636,24 @@ def is_local_endpoint(base_url: str) -> bool:
return False
def _localhost_to_ipv4(url: str) -> str:
"""Rewrite a ``localhost`` host to ``127.0.0.1`` in a probe URL.
On Windows dual-stack machines, httpx resolves ``localhost`` to ``::1``
first and pays a ~2s IPv6 connect timeout before falling back to IPv4
when the local server only listens on IPv4 (LM Studio, Ollama defaults).
Probing the IPv4 loopback directly skips that penalty. Non-localhost
URLs pass through unchanged.
"""
if not url:
return url
url = url.replace("://localhost:", "://127.0.0.1:")
url = url.replace("://localhost/", "://127.0.0.1/")
if url.endswith("://localhost"):
url = url[:-len("localhost")] + "127.0.0.1"
return url
def detect_local_server_type(base_url: str, api_key: str = "") -> Optional[str]:
"""Detect which local server is running at base_url by probing known endpoints.
@ -652,10 +670,7 @@ def detect_local_server_type(base_url: str, api_key: str = "") -> Optional[str]:
# Resolve localhost to IPv4 to avoid 2s IPv6 timeout on Windows dual-stack.
# Applied to ``normalized`` before deriving server/LM Studio URLs AND
# before the cache lookup, so localhost and 127.0.0.1 share a cache entry.
normalized = normalized.replace("://localhost:", "://127.0.0.1:")
normalized = normalized.replace("://localhost/", "://127.0.0.1/")
if normalized.endswith("://localhost"):
normalized = normalized[:-len("localhost")] + "127.0.0.1"
normalized = _localhost_to_ipv4(normalized)
server_url = normalized
if server_url.endswith("/v1"):
@ -1393,7 +1408,7 @@ def query_ollama_num_ctx(model: str, base_url: str, api_key: str = "") -> Option
import httpx
bare_model = _strip_provider_prefix(model)
server_url = base_url.rstrip("/")
server_url = _localhost_to_ipv4(base_url.rstrip("/"))
if server_url.endswith("/v1"):
server_url = server_url[:-3]
@ -1454,7 +1469,7 @@ def query_ollama_supports_vision(model: str, base_url: str, api_key: str = "") -
except Exception:
return None
server_url = base_url.rstrip("/")
server_url = _localhost_to_ipv4(base_url.rstrip("/"))
if server_url.endswith("/v1"):
server_url = server_url[:-3]
@ -1531,7 +1546,7 @@ def _query_ollama_api_show_uncached(model: str, base_url: str, api_key: str = ""
"""Uncached body of ``_query_ollama_api_show`` — one POST to ``/api/show``."""
import httpx
server_url = base_url.rstrip("/")
server_url = _localhost_to_ipv4(base_url.rstrip("/"))
if server_url.endswith("/v1"):
server_url = server_url[:-3]
@ -1650,10 +1665,10 @@ def _query_local_context_length_uncached(model: str, base_url: str, api_key: str
model = _strip_provider_prefix(model)
# Strip /v1 suffix to get the server root
server_url = base_url.rstrip("/")
server_url = _localhost_to_ipv4(base_url.rstrip("/"))
if server_url.endswith("/v1"):
server_url = server_url[:-3]
lmstudio_url = _lmstudio_server_root(base_url)
lmstudio_url = _localhost_to_ipv4(_lmstudio_server_root(base_url))
headers = _auth_headers(api_key)

View file

@ -465,7 +465,7 @@ class TestQueryLocalContextLengthLmStudio:
result = _query_local_context_length("publisher/model-a", "http://localhost:1234/api/v1")
assert result == 32768
assert client_mock.get.call_args_list[0].args[0] == "http://localhost:1234/api/v1/models"
assert client_mock.get.call_args_list[0].args[0] == "http://127.0.0.1:1234/api/v1/models"
class TestDetectLocalServerTypeAuth:

View file

@ -87,6 +87,41 @@ class TestOllamaApiShowCaching:
assert client.post.call_count == 1
class TestLocalhostIPv4SiblingSites:
"""#37595 widened: every probe helper rewrites localhost→127.0.0.1,
not just detect_local_server_type."""
def test_helper_rewrites_all_forms(self):
from agent.model_metadata import _localhost_to_ipv4
assert _localhost_to_ipv4("http://localhost:1234/v1") == "http://127.0.0.1:1234/v1"
assert _localhost_to_ipv4("http://localhost/v1") == "http://127.0.0.1/v1"
assert _localhost_to_ipv4("http://localhost") == "http://127.0.0.1"
# Non-localhost passes through untouched.
assert _localhost_to_ipv4("http://192.168.1.10:8080") == "http://192.168.1.10:8080"
assert _localhost_to_ipv4("https://api.openai.com/v1") == "https://api.openai.com/v1"
assert _localhost_to_ipv4("") == ""
def test_ollama_api_show_probes_ipv4(self):
from agent.model_metadata import _query_ollama_api_show
client = _client_mock(_mock_show_response(131072))
with patch("httpx.Client", return_value=client):
_query_ollama_api_show("llama3", "http://localhost:11434")
assert client.post.call_args[0][0].startswith("http://127.0.0.1:11434")
def test_query_ollama_num_ctx_probes_ipv4(self):
from agent.model_metadata import query_ollama_num_ctx
client = _client_mock(_mock_show_response(131072))
with patch("agent.model_metadata.detect_local_server_type", return_value="ollama"), \
patch("httpx.Client", return_value=client):
query_ollama_num_ctx("llama3", "http://localhost:11434")
assert client.post.call_args[0][0].startswith("http://127.0.0.1:11434")
class TestContextCacheKeyNormalization:
def test_trailing_slash_variants_share_one_entry(self, tmp_path, monkeypatch):
from agent import model_metadata