From fb6dd28ac3e1031cb052cf498ef2e483fd34e492 Mon Sep 17 00:00:00 2001 From: AJV20 <24819659+AJV20@users.noreply.github.com> Date: Wed, 27 May 2026 12:19:42 -0400 Subject: [PATCH] fix: cap custom models endpoint probes --- CHANGELOG.md | 4 ++++ api/config.py | 8 +++++++- tests/test_issue2540_models_endpoint_error.py | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c365129f..c4a56117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Custom provider `/v1/models` discovery now uses a short per-endpoint timeout and gracefully skips slow or unreachable providers, reducing cold `/api/models` cache rebuild latency. (#3024) + ## [v0.51.145] — 2026-05-26 — Release DQ (stage-batch27 — sidebar running-state preservation) ### Fixed diff --git a/api/config.py b/api/config.py index 103c9f21..34dd5e90 100644 --- a/api/config.py +++ b/api/config.py @@ -70,6 +70,12 @@ PROJECTS_FILE = STATE_DIR / "projects.json" logger = logging.getLogger(__name__) +# Keep custom provider /v1/models probes below the frontend's generic request +# timeout even when one upstream is slow or unreachable. The models cache rebuild +# path probes configured custom endpoints serially, so each provider needs a +# short hard cap and graceful degradation. +CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS = 5.0 + def _env_mb_bytes(name: str, default_mb: int) -> int: """Parse an optional megabyte environment variable into bytes. @@ -3653,7 +3659,7 @@ def get_available_models() -> dict: req.add_header("User-Agent", "OpenAI/Python 1.0") for k, v in headers.items(): req.add_header(k, v) - with urllib.request.urlopen(req, timeout=10) as response: # nosec B310 + with urllib.request.urlopen(req, timeout=CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS) as response: # nosec B310 data = json.loads(response.read().decode("utf-8")) return _extract_model_entries_from_payload(data, provider), None except urllib.error.HTTPError as exc: diff --git a/tests/test_issue2540_models_endpoint_error.py b/tests/test_issue2540_models_endpoint_error.py index 89dce0a0..b83cd9bc 100644 --- a/tests/test_issue2540_models_endpoint_error.py +++ b/tests/test_issue2540_models_endpoint_error.py @@ -112,6 +112,25 @@ def test_named_custom_provider_models_endpoint_5xx_preserves_status(monkeypatch, assert error["code"] == 502 assert "returned 502" in error["message"] +def test_named_custom_provider_models_endpoint_network_error_uses_short_timeout(monkeypatch, tmp_path): + observed_timeouts = [] + + def fake_urlopen(req, timeout=10): + observed_timeouts.append(timeout) + raise urllib.error.URLError("timed out") + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + with _ConfigState(): + _configure_named_custom_provider(tmp_path, monkeypatch) + data = config.get_available_models() + + group = _groups_by_provider(data)["custom:broken-proxy"] + assert group["models"] == [] + assert group["models_endpoint_error"]["kind"] == "network" + assert observed_timeouts == [config.CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS] + assert max(observed_timeouts) <= 5.0 + def test_frontend_model_picker_renders_provider_endpoint_hint(): ui = open("static/ui.js", encoding="utf-8").read()