fix(model_metadata): rewrite localhost->IPv4 for the remaining local probe sites

fetch_endpoint_model_metadata's generic (non-LM-Studio) /models fetch and
its llama.cpp /v1/props context-length follow-up built request URLs
straight from the unrewritten candidate, unlike every other local-probe
site. Both retained the multi-second dual-stack IPv6 connect penalty
that _localhost_to_ipv4() exists to skip (measured on macOS: localhost
32.9ms vs 127.0.0.1 0.1ms on a dead port; ~2s on Windows). normalized
stays the cache key so caching behavior is unchanged; only the outbound
request target is rewritten.

Re-derived from PR #61528 onto current main (original no longer applied
cleanly).
This commit is contained in:
pierrenode 2026-08-03 16:40:48 +05:30 committed by kshitij
parent d5584a32d8
commit fc32a38c3a
2 changed files with 61 additions and 2 deletions

View File

@ -1184,7 +1184,11 @@ def fetch_endpoint_model_metadata(
last_error = exc
for candidate in candidates:
url = candidate.rstrip("/") + "/models"
# normalized/candidates stay unrewritten (cache key stability); only
# the outbound request target is IPv4-resolved to skip the multi-second
# dual-stack IPv6 connect timeout (see _localhost_to_ipv4).
request_candidate = _localhost_to_ipv4(candidate)
url = request_candidate.rstrip("/") + "/models"
response = None
try:
response = requests.get(
@ -1230,7 +1234,7 @@ def fetch_endpoint_model_metadata(
if is_llamacpp:
try:
# Try /v1/props first (current llama.cpp); fall back to /props for older builds
base = candidate.rstrip("/").replace("/v1", "")
base = request_candidate.rstrip("/").replace("/v1", "")
_verify = _resolve_requests_verify()
props_resp = requests.get(base + "/v1/props", headers=headers, timeout=5, verify=_verify)
if not props_resp.ok:

View File

@ -186,6 +186,61 @@ class TestLocalhostIPv4SiblingSites:
assert client.post.call_args[0][0].startswith("http://127.0.0.1:11434")
def test_fetch_endpoint_model_metadata_generic_probe_uses_ipv4(self):
"""The generic (non-LM-Studio) /models fetch loop must also rewrite
localhost->127.0.0.1 before probing, like the LM Studio branch above."""
from agent import model_metadata
from agent.model_metadata import fetch_endpoint_model_metadata
model_metadata._endpoint_model_metadata_cache.clear()
model_metadata._endpoint_model_metadata_cache_time.clear()
resp = MagicMock()
resp.status_code = 200
resp.raise_for_status = MagicMock()
resp.json.return_value = {"data": []}
with patch("agent.model_metadata.detect_local_server_type", return_value=None), \
patch("agent.model_metadata.requests.get", return_value=resp) as mock_get:
fetch_endpoint_model_metadata("http://localhost:8000/v1")
assert mock_get.call_args[0][0].startswith("http://127.0.0.1:8000")
def test_fetch_endpoint_model_metadata_llamacpp_props_followup_uses_ipv4(self):
"""The llama.cpp /props context-length follow-up must also rewrite
localhost->127.0.0.1 before probing, not just the initial /models call."""
from agent import model_metadata
from agent.model_metadata import fetch_endpoint_model_metadata
model_metadata._endpoint_model_metadata_cache.clear()
model_metadata._endpoint_model_metadata_cache_time.clear()
models_resp = MagicMock()
models_resp.status_code = 200
models_resp.raise_for_status = MagicMock()
models_resp.json.return_value = {
"data": [{"id": "llama-3-8b", "owned_by": "llamacpp"}],
}
props_resp = MagicMock()
props_resp.ok = True
props_resp.json.return_value = {
"default_generation_settings": {"n_ctx": 32768},
"model_alias": "llama-3-8b",
}
with patch("agent.model_metadata.detect_local_server_type", return_value=None), \
patch(
"agent.model_metadata.requests.get",
side_effect=[models_resp, props_resp],
) as mock_get:
result = fetch_endpoint_model_metadata("http://localhost:8000/v1")
assert mock_get.call_count == 2
props_call_url = mock_get.call_args_list[1][0][0]
assert props_call_url.startswith("http://127.0.0.1:8000")
assert result["llama-3-8b"]["context_length"] == 32768
class TestContextCacheKeyNormalization: