From 013779924fc5e682a2833ae35689be0645d11b19 Mon Sep 17 00:00:00 2001 From: Josh Tsai <128559392+bounce12340@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:35:30 +0800 Subject: [PATCH] fix(agent): fail fast on custom-provider /models auth errors - Short-circuit the candidate waterfall on HTTP 401/403: an auth wall proves the endpoint family exists, so probing the alternate URL just doubles the wasted wait (the reported endpoint takes ~10s to return 401 without a key). - Stream the probe so 4xx never downloads a slow error body; responses are closed on every exit path. - Regression tests: single-call assertion on 401/403 (fails on main), negative-cache reuse, 404 waterfall preserved, no .json() on 4xx. Fixes #69905 Co-Authored-By: Claude Fable 5 --- agent/model_metadata.py | 19 +++++++- tests/agent/test_model_metadata.py | 73 ++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 42d841009fa3d..6643ec820a6ed 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -1185,8 +1185,22 @@ def fetch_endpoint_model_metadata( for candidate in candidates: url = candidate.rstrip("/") + "/models" + response = None try: - response = requests.get(url, headers=headers, timeout=(5, 10), verify=_resolve_requests_verify()) + response = requests.get( + url, + headers=headers, + timeout=(5, 10), + verify=_resolve_requests_verify(), + stream=True, + ) + if response.status_code in (401, 403): + logger.debug( + "Model metadata probe received HTTP %s from %s; stopping candidate probing", + response.status_code, + url, + ) + break response.raise_for_status() payload = response.json() cache: Dict[str, Dict[str, Any]] = {} @@ -1236,6 +1250,9 @@ def fetch_endpoint_model_metadata( return cache except Exception as exc: last_error = exc + finally: + if response is not None: + response.close() if last_error: logger.debug("Failed to fetch model metadata from %s/models: %s", normalized, last_error) diff --git a/tests/agent/test_model_metadata.py b/tests/agent/test_model_metadata.py index ac8ed899cc3e4..874b97d5dddcd 100644 --- a/tests/agent/test_model_metadata.py +++ b/tests/agent/test_model_metadata.py @@ -489,6 +489,79 @@ class TestCodexOAuthContextLength: +# ========================================================================= +# Custom endpoint model metadata +# ========================================================================= + +class TestFetchEndpointModelMetadata: + def setup_method(self): + import agent.model_metadata as mm + mm._endpoint_model_metadata_cache.clear() + mm._endpoint_model_metadata_cache_time.clear() + + @pytest.mark.parametrize("status_code", [401, 403]) + def test_auth_failure_stops_after_first_candidate(self, status_code): + import agent.model_metadata as mm + + response = MagicMock() + response.status_code = status_code + response.raise_for_status.side_effect = RuntimeError(str(status_code)) + + with patch("agent.model_metadata.requests.get", return_value=response) as mock_get: + result = mm.fetch_endpoint_model_metadata("https://custom.example/v1") + + assert result == {} + mock_get.assert_called_once() + assert mock_get.call_args.kwargs["stream"] is True + response.raise_for_status.assert_not_called() + response.json.assert_not_called() + response.close.assert_called_once() + + def test_auth_failure_empty_result_is_cached(self): + import agent.model_metadata as mm + + response = MagicMock() + response.status_code = 401 + response.raise_for_status.side_effect = RuntimeError("401") + + with patch("agent.model_metadata.requests.get", return_value=response) as mock_get: + first = mm.fetch_endpoint_model_metadata("https://custom.example/v1") + second = mm.fetch_endpoint_model_metadata("https://custom.example/v1") + + assert first == second == {} + mock_get.assert_called_once() + response.close.assert_called_once() + + def test_not_found_still_tries_alternate_candidate(self): + import agent.model_metadata as mm + + not_found = MagicMock() + not_found.status_code = 404 + not_found.raise_for_status.side_effect = RuntimeError("404") + success = MagicMock() + success.status_code = 200 + success.json.return_value = { + "data": [{"id": "test/model", "context_length": 32768}] + } + + with patch( + "agent.model_metadata.requests.get", + side_effect=[not_found, success], + ) as mock_get: + result = mm.fetch_endpoint_model_metadata("https://custom.example/v1") + + assert result["test/model"]["context_length"] == 32768 + assert mock_get.call_count == 2 + assert [call.args[0] for call in mock_get.call_args_list] == [ + "https://custom.example/v1/models", + "https://custom.example/models", + ] + assert all(call.kwargs["stream"] is True for call in mock_get.call_args_list) + not_found.json.assert_not_called() + not_found.close.assert_called_once() + success.close.assert_called_once() + + # ========================================================================= # Nous Portal context-window resolution (provider="nous") # =========================================================================