From 26b1b283b0c20f2e364520d68e1fc00be25897c7 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Thu, 3 Sep 2026 10:18:59 +0000 Subject: [PATCH 1/5] feat(openai_api_compatible): support custom rerank endpoint URL --- models/openai_api_compatible/manifest.yaml | 2 +- .../models/rerank/rerank.py | 21 ++-- .../provider/openai_api_compatible.yaml | 15 +++ .../tests/test_rerank_authorization.py | 102 +++++++++++++++++- 4 files changed, 130 insertions(+), 10 deletions(-) diff --git a/models/openai_api_compatible/manifest.yaml b/models/openai_api_compatible/manifest.yaml index ccf24f6bb..a008e8935 100644 --- a/models/openai_api_compatible/manifest.yaml +++ b/models/openai_api_compatible/manifest.yaml @@ -1,4 +1,4 @@ -version: 0.0.64 +version: 0.0.65 type: plugin author: "langgenius" name: "openai_api_compatible" diff --git a/models/openai_api_compatible/models/rerank/rerank.py b/models/openai_api_compatible/models/rerank/rerank.py index a65dad943..3de627167 100644 --- a/models/openai_api_compatible/models/rerank/rerank.py +++ b/models/openai_api_compatible/models/rerank/rerank.py @@ -27,6 +27,15 @@ class OpenAIRerankModel(OAICompatRerankModel): + @staticmethod + def _resolve_rerank_endpoint_url(credentials: Mapping) -> str: + rerank_endpoint_url = (credentials.get("rerank_endpoint_url") or "").strip() + if rerank_endpoint_url: + return rerank_endpoint_url + + endpoint_url = credentials.get("endpoint_url", "").rstrip("/") + return f"{endpoint_url}/rerank" + def validate_credentials(self, model: str, credentials: dict) -> None: """ Validate model credentials @@ -91,7 +100,7 @@ def _invoke( return RerankResult(model=model, docs=[]) # Build API request - endpoint_url = credentials.get("endpoint_url", "").rstrip("/") + rerank_endpoint_url = self._resolve_rerank_endpoint_url(credentials) api_key = credentials.get("api_key", "") endpoint_model_name = credentials.get("endpoint_model_name", "") or model @@ -112,10 +121,10 @@ def _invoke( } try: - logger.info(f"Rerank API Request (text mode) to {endpoint_url}/rerank") + logger.info(f"Rerank API Request (text mode) to {rerank_endpoint_url}") response = requests.post( - f"{endpoint_url}/rerank", + rerank_endpoint_url, headers=headers, json=payload, timeout=60, @@ -216,7 +225,7 @@ def _invoke_multimodal( ) # Build API request - endpoint_url = credentials.get("endpoint_url", "").rstrip("/") + rerank_endpoint_url = self._resolve_rerank_endpoint_url(credentials) api_key = credentials.get("api_key", "") endpoint_model_name = credentials.get("endpoint_model_name", "") or model @@ -247,14 +256,14 @@ def _invoke_multimodal( try: logger.info( - f"Rerank API Request (multimodal mode) to {endpoint_url}/rerank" + f"Rerank API Request (multimodal mode) to {rerank_endpoint_url}" ) logger.info(f"Query: {query_text[:100]}") logger.info(f"Documents count: {len(documents_params)}") logger.debug(f"Payload: {json.dumps(payload, ensure_ascii=False)[:1000]}") response = requests.post( - f"{endpoint_url}/rerank", + rerank_endpoint_url, headers=headers, json=payload, timeout=60, diff --git a/models/openai_api_compatible/provider/openai_api_compatible.yaml b/models/openai_api_compatible/provider/openai_api_compatible.yaml index 2359c315b..5a444ebd9 100644 --- a/models/openai_api_compatible/provider/openai_api_compatible.yaml +++ b/models/openai_api_compatible/provider/openai_api_compatible.yaml @@ -92,6 +92,21 @@ model_credential_schema: placeholder: zh_Hans: Base URL, e.g. https://api.openai.com/v1 en_US: Base URL, e.g. https://api.openai.com/v1 + - variable: rerank_endpoint_url + label: + zh_Hans: 重排序端点 URL + en_US: Rerank Endpoint URL + type: text-input + required: false + show_on: + - variable: __model_type + value: rerank + placeholder: + zh_Hans: https://gateway.example.com/v1/reranks + en_US: https://gateway.example.com/v1/reranks + help: + zh_Hans: 可选的完整重排序端点。留空时使用 API Base URL + /rerank。请勿在 URL 中包含凭据或令牌。 + en_US: Optional complete rerank endpoint. When empty, API Base URL + /rerank is used. Do not include credentials or tokens in the URL. - variable: endpoint_model_name label: zh_Hans: API endpoint中的模型名称 diff --git a/models/openai_api_compatible/tests/test_rerank_authorization.py b/models/openai_api_compatible/tests/test_rerank_authorization.py index f6f4a0331..879c284cf 100644 --- a/models/openai_api_compatible/tests/test_rerank_authorization.py +++ b/models/openai_api_compatible/tests/test_rerank_authorization.py @@ -1,4 +1,4 @@ -"""Regression for issue #1724. +"""Regression tests for rerank request construction. The official OpenAI-API-compatible plugin rerank implementation used to emit an empty `Authorization: ` header when the API key was missing, @@ -6,8 +6,8 @@ only when an API key is truthy. These tests drive the model directly so we can capture the headers that -the implementation actually sends, without needing a real rerank -gateway. +the implementation actually sends and the URL it targets, without needing +a real rerank gateway. """ from unittest.mock import MagicMock, patch @@ -40,6 +40,73 @@ def _credentials(**overrides): return creds +@pytest.mark.parametrize( + "overrides", + [ + {}, + {"rerank_endpoint_url": ""}, + {"rerank_endpoint_url": " \t "}, + ], + ids=["missing", "empty", "whitespace"], +) +def test_text_rerank_uses_legacy_endpoint_when_custom_endpoint_is_empty(overrides): + model = OpenAIRerankModel(model_schemas=[]) + with patch("models.rerank.rerank.requests.post") as mock_post: + mock_post.return_value = MagicMock( + status_code=200, + json=lambda: {"results": []}, + ) + model._invoke( + model="bge-reranker-v2-m3", + credentials=_credentials(**overrides), + query="q", + docs=["d1"], + ) + + req = _captured_request(mock_post) + assert req["url"] == "https://rerank.example.com/v1/rerank" + + +def test_text_rerank_uses_custom_endpoint_url_exactly_after_trimming(): + model = OpenAIRerankModel(model_schemas=[]) + with patch("models.rerank.rerank.requests.post") as mock_post: + mock_post.return_value = MagicMock( + status_code=200, + json=lambda: {"results": []}, + ) + model._invoke( + model="bge-reranker-v2-m3", + credentials=_credentials( + rerank_endpoint_url=( + " https://gateway.example.com/v1/reranks/?route=qwen " + ) + ), + query="q", + docs=["d1"], + ) + + req = _captured_request(mock_post) + assert req["url"] == "https://gateway.example.com/v1/reranks/?route=qwen" + + +def test_credential_validation_uses_custom_endpoint_url(): + model = OpenAIRerankModel(model_schemas=[]) + with patch("models.rerank.rerank.requests.post") as mock_post: + mock_post.return_value = MagicMock( + status_code=200, + json=lambda: {"results": []}, + ) + model.validate_credentials( + model="bge-reranker-v2-m3", + credentials=_credentials( + rerank_endpoint_url="https://gateway.example.com/v1/reranks" + ), + ) + + req = _captured_request(mock_post) + assert req["url"] == "https://gateway.example.com/v1/reranks" + + def test_text_rerank_omits_authorization_when_api_key_missing(): model = OpenAIRerankModel(model_schemas=[]) with patch("models.rerank.rerank.requests.post") as mock_post: @@ -148,3 +215,32 @@ def test_multimodal_rerank_includes_bearer_when_api_key_present(): ) req = _captured_request(mock_post) assert req["headers"]["Authorization"] == "Bearer sk-test-5678" + + +def test_multimodal_rerank_uses_custom_endpoint_url(): + from dify_plugin.entities.model.text_embedding import ( + MultiModalContent, + MultiModalContentType, + ) + + model = OpenAIRerankModel(model_schemas=[]) + query = MultiModalContent( + content_type=MultiModalContentType.TEXT, content="q" + ) + docs = [MultiModalContent(content_type=MultiModalContentType.TEXT, content="d1")] + with patch("models.rerank.rerank.requests.post") as mock_post: + mock_post.return_value = MagicMock( + status_code=200, + json=lambda: {"results": []}, + ) + model._invoke_multimodal( + model="qwen3-vl-reranker", + credentials=_credentials( + rerank_endpoint_url="https://gateway.example.com/v1/reranks" + ), + query=query, + docs=docs, + ) + + req = _captured_request(mock_post) + assert req["url"] == "https://gateway.example.com/v1/reranks" From 6e11dc33b9cdac20a8727651fd60b52f66e2de66 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Thu, 3 Sep 2026 10:43:59 +0000 Subject: [PATCH 2/5] fix(openai_api_compatible): avoid logging rerank endpoint URLs --- models/openai_api_compatible/models/rerank/rerank.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/models/openai_api_compatible/models/rerank/rerank.py b/models/openai_api_compatible/models/rerank/rerank.py index 3de627167..702a8be46 100644 --- a/models/openai_api_compatible/models/rerank/rerank.py +++ b/models/openai_api_compatible/models/rerank/rerank.py @@ -121,7 +121,7 @@ def _invoke( } try: - logger.info(f"Rerank API Request (text mode) to {rerank_endpoint_url}") + logger.info("Rerank API Request (text mode)") response = requests.post( rerank_endpoint_url, @@ -255,9 +255,7 @@ def _invoke_multimodal( } try: - logger.info( - f"Rerank API Request (multimodal mode) to {rerank_endpoint_url}" - ) + logger.info("Rerank API Request (multimodal mode)") logger.info(f"Query: {query_text[:100]}") logger.info(f"Documents count: {len(documents_params)}") logger.debug(f"Payload: {json.dumps(payload, ensure_ascii=False)[:1000]}") From f420523b5a38b74e9e3a08be9d4f870490684792 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Thu, 3 Sep 2026 10:48:21 +0000 Subject: [PATCH 3/5] fix(openai_api_compatible): hide rerank URLs from request errors --- .../models/rerank/rerank.py | 8 +++---- .../tests/test_rerank_authorization.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/models/openai_api_compatible/models/rerank/rerank.py b/models/openai_api_compatible/models/rerank/rerank.py index 702a8be46..2ff6ea66a 100644 --- a/models/openai_api_compatible/models/rerank/rerank.py +++ b/models/openai_api_compatible/models/rerank/rerank.py @@ -172,8 +172,8 @@ def _invoke( docs=rerank_docs, ) - except requests.exceptions.RequestException as ex: - raise InvokeServerUnavailableError(str(ex)) + except requests.exceptions.RequestException: + raise InvokeServerUnavailableError("Rerank API request failed") from None except Exception as ex: raise InvokeError(str(ex)) @@ -314,8 +314,8 @@ def _invoke_multimodal( docs=rerank_docs, ) - except requests.exceptions.RequestException as ex: - raise InvokeServerUnavailableError(str(ex)) + except requests.exceptions.RequestException: + raise InvokeServerUnavailableError("Rerank API request failed") from None except Exception as ex: raise InvokeError(str(ex)) diff --git a/models/openai_api_compatible/tests/test_rerank_authorization.py b/models/openai_api_compatible/tests/test_rerank_authorization.py index 879c284cf..c7fc4ad3a 100644 --- a/models/openai_api_compatible/tests/test_rerank_authorization.py +++ b/models/openai_api_compatible/tests/test_rerank_authorization.py @@ -10,9 +10,12 @@ a real rerank gateway. """ +import traceback from unittest.mock import MagicMock, patch import pytest +import requests +from dify_plugin.errors.model import InvokeServerUnavailableError from models.rerank.rerank import OpenAIRerankModel @@ -107,6 +110,25 @@ def test_credential_validation_uses_custom_endpoint_url(): assert req["url"] == "https://gateway.example.com/v1/reranks" +def test_request_error_does_not_expose_custom_endpoint_url(): + endpoint_url = "https://user:password@gateway.example.com/v1/reranks?token=secret" + model = OpenAIRerankModel(model_schemas=[]) + with patch( + "models.rerank.rerank.requests.post", + side_effect=requests.exceptions.ConnectionError(endpoint_url), + ): + with pytest.raises(InvokeServerUnavailableError) as exc_info: + model._invoke( + model="bge-reranker-v2-m3", + credentials=_credentials(rerank_endpoint_url=endpoint_url), + query="q", + docs=["d1"], + ) + + assert str(exc_info.value) == "Rerank API request failed" + assert endpoint_url not in "".join(traceback.format_exception(exc_info.value)) + + def test_text_rerank_omits_authorization_when_api_key_missing(): model = OpenAIRerankModel(model_schemas=[]) with patch("models.rerank.rerank.requests.post") as mock_post: From 85c974bda8774a259f6acc6e755c638cc38994be Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Mon, 7 Sep 2026 14:05:25 +0000 Subject: [PATCH 4/5] fix(openai_api_compatible): harden rerank diagnostics --- .../models/rerank/rerank.py | 65 +++++++-------- .../provider/openai_api_compatible.yaml | 7 +- .../tests/test_rerank_authorization.py | 83 ++++++++++++++++++- 3 files changed, 116 insertions(+), 39 deletions(-) diff --git a/models/openai_api_compatible/models/rerank/rerank.py b/models/openai_api_compatible/models/rerank/rerank.py index 2ff6ea66a..cd6b5d632 100644 --- a/models/openai_api_compatible/models/rerank/rerank.py +++ b/models/openai_api_compatible/models/rerank/rerank.py @@ -1,9 +1,7 @@ import base64 import ipaddress -import json import logging -import re -from typing import Mapping, Optional, Union, Any +from typing import Mapping, Optional from urllib.parse import urlparse import requests @@ -36,6 +34,17 @@ def _resolve_rerank_endpoint_url(credentials: Mapping) -> str: endpoint_url = credentials.get("endpoint_url", "").rstrip("/") return f"{endpoint_url}/rerank" + @staticmethod + def _safe_request_error_detail( + error: requests.exceptions.RequestException, + ) -> str: + if ( + isinstance(error, requests.exceptions.HTTPError) + and error.response is not None + ): + return f"HTTP {error.response.status_code}" + return type(error).__name__ + def validate_credentials(self, model: str, credentials: dict) -> None: """ Validate model credentials @@ -130,11 +139,6 @@ def _invoke( timeout=60, ) - if response.status_code != 200: - logger.error( - f"Rerank API Error {response.status_code}: {response.text[:1000]}" - ) - response.raise_for_status() result = response.json() @@ -172,8 +176,11 @@ def _invoke( docs=rerank_docs, ) - except requests.exceptions.RequestException: - raise InvokeServerUnavailableError("Rerank API request failed") from None + except requests.exceptions.RequestException as ex: + detail = self._safe_request_error_detail(ex) + raise InvokeServerUnavailableError( + f"Rerank API request failed ({detail})" + ) from None except Exception as ex: raise InvokeError(str(ex)) @@ -237,13 +244,7 @@ def _invoke_multimodal( headers["Authorization"] = f"Bearer {api_key}" # Convert documents to ScoreMultiModalParam format - documents_params = [] - for i, doc in enumerate(docs): - doc_param = self._to_score_multimodal_param(doc) - documents_params.append(doc_param) - logger.debug( - f"Document {i}: {json.dumps(doc_param, ensure_ascii=False)[:200]}" - ) + documents_params = [self._to_score_multimodal_param(doc) for doc in docs] # Build payload according to vLLM/Qwen3 format # Use `top_n if top_n is not None else len(docs)` to correctly handle top_n=0 @@ -255,10 +256,10 @@ def _invoke_multimodal( } try: - logger.info("Rerank API Request (multimodal mode)") - logger.info(f"Query: {query_text[:100]}") - logger.info(f"Documents count: {len(documents_params)}") - logger.debug(f"Payload: {json.dumps(payload, ensure_ascii=False)[:1000]}") + logger.info( + "Rerank API Request (multimodal mode): %d documents", + len(documents_params), + ) response = requests.post( rerank_endpoint_url, @@ -267,11 +268,6 @@ def _invoke_multimodal( timeout=60, ) - if response.status_code != 200: - logger.error( - f"Rerank API Error {response.status_code}: {response.text[:1000]}" - ) - response.raise_for_status() result = response.json() @@ -314,8 +310,11 @@ def _invoke_multimodal( docs=rerank_docs, ) - except requests.exceptions.RequestException: - raise InvokeServerUnavailableError("Rerank API request failed") from None + except requests.exceptions.RequestException as ex: + detail = self._safe_request_error_detail(ex) + raise InvokeServerUnavailableError( + f"Rerank API request failed ({detail})" + ) from None except Exception as ex: raise InvokeError(str(ex)) @@ -337,7 +336,7 @@ def _validate_image_url(self, url: str) -> str: # Only allow http/https URLs if not (url.startswith("http://") or url.startswith("https://")): - logger.warning(f"Blocked non-HTTP URL: {url[:50]}...") + logger.warning("Blocked non-HTTP image URL") return "" # Parse URL to check for SSRF attempts @@ -350,7 +349,7 @@ def _validate_image_url(self, url: str) -> str: # Block localhost if hostname in ("localhost", "127.0.0.1", "::1"): - logger.warning(f"Blocked localhost URL: {url[:50]}...") + logger.warning("Blocked localhost image URL") return "" # Block private IP ranges @@ -363,15 +362,15 @@ def _validate_image_url(self, url: str) -> str: or ip.is_link_local or ip.is_reserved ): - logger.warning(f"Blocked private IP URL: {url[:50]}...") + logger.warning("Blocked private IP image URL") return "" except ValueError: # Not an IP address, it's a hostname - allow it pass return url - except Exception as e: - logger.warning(f"URL validation failed: {e}") + except Exception: + logger.warning("Image URL validation failed") return "" def _to_score_multimodal_param(self, content: MultiModalContent) -> dict: diff --git a/models/openai_api_compatible/provider/openai_api_compatible.yaml b/models/openai_api_compatible/provider/openai_api_compatible.yaml index 71514e7d5..9dcedfc91 100644 --- a/models/openai_api_compatible/provider/openai_api_compatible.yaml +++ b/models/openai_api_compatible/provider/openai_api_compatible.yaml @@ -84,11 +84,8 @@ model_credential_schema: - variable: __model_type value: rerank placeholder: - zh_Hans: https://gateway.example.com/v1/reranks - en_US: https://gateway.example.com/v1/reranks - help: - zh_Hans: 可选的完整重排序端点。留空时使用 API Base URL + /rerank。请勿在 URL 中包含凭据或令牌。 - en_US: Optional complete rerank endpoint. When empty, API Base URL + /rerank is used. Do not include credentials or tokens in the URL. + zh_Hans: 可选完整 URL;留空使用 API Base URL + /rerank;请勿包含凭据或令牌 + en_US: Optional complete URL; defaults to API Base URL + /rerank; do not include credentials or tokens - variable: endpoint_model_name label: zh_Hans: API endpoint中的模型名称 diff --git a/models/openai_api_compatible/tests/test_rerank_authorization.py b/models/openai_api_compatible/tests/test_rerank_authorization.py index c7fc4ad3a..a27574317 100644 --- a/models/openai_api_compatible/tests/test_rerank_authorization.py +++ b/models/openai_api_compatible/tests/test_rerank_authorization.py @@ -10,6 +10,7 @@ a real rerank gateway. """ +import logging import traceback from unittest.mock import MagicMock, patch @@ -125,10 +126,90 @@ def test_request_error_does_not_expose_custom_endpoint_url(): docs=["d1"], ) - assert str(exc_info.value) == "Rerank API request failed" + assert str(exc_info.value) == "Rerank API request failed (ConnectionError)" assert endpoint_url not in "".join(traceback.format_exception(exc_info.value)) +def test_http_error_exposes_only_status_code(caplog): + endpoint_url = "https://user:password@gateway.example.com/rerank?token=secret" + response = requests.Response() + response.status_code = 401 + response.url = endpoint_url + response.reason = "Unauthorized" + response._content = b"response-secret" + model = OpenAIRerankModel(model_schemas=[]) + caplog.set_level(logging.DEBUG, logger="models.rerank.rerank") + + with patch("models.rerank.rerank.requests.post", return_value=response): + with pytest.raises(InvokeServerUnavailableError) as exc_info: + model._invoke( + model="bge-reranker-v2-m3", + credentials=_credentials(rerank_endpoint_url=endpoint_url), + query="q", + docs=["d1"], + ) + + error_output = "".join(traceback.format_exception(exc_info.value)) + assert str(exc_info.value) == "Rerank API request failed (HTTP 401)" + assert endpoint_url not in error_output + assert "response-secret" not in error_output + caplog.text + + +def test_rerank_logs_exclude_multimodal_content_and_image_urls(caplog): + from dify_plugin.entities.model.text_embedding import ( + MultiModalContent, + MultiModalContentType, + ) + + model = OpenAIRerankModel(model_schemas=[]) + caplog.set_level(logging.DEBUG, logger="models.rerank.rerank") + query = MultiModalContent( + content_type=MultiModalContentType.TEXT, + content="query-secret", + ) + docs = [ + MultiModalContent( + content_type=MultiModalContentType.TEXT, + content="document-secret", + ), + MultiModalContent( + content_type=MultiModalContentType.IMAGE, + content="file://image-secret", + ), + ] + + with patch("models.rerank.rerank.requests.post") as mock_post: + mock_post.return_value = MagicMock( + status_code=200, + json=lambda: {"results": []}, + ) + model._invoke_multimodal( + model="qwen3-vl-reranker", + credentials=_credentials(), + query=query, + docs=docs, + ) + + model._validate_image_url("http://localhost/localhost-secret") + model._validate_image_url("http://10.0.0.1/private-secret") + with patch( + "models.rerank.rerank.urlparse", + side_effect=ValueError("parse-secret"), + ): + model._validate_image_url("https://example.com/url-secret") + + secrets = ( + "query-secret", + "document-secret", + "image-secret", + "localhost-secret", + "private-secret", + "url-secret", + "parse-secret", + ) + assert all(secret not in caplog.text for secret in secrets) + + def test_text_rerank_omits_authorization_when_api_key_missing(): model = OpenAIRerankModel(model_schemas=[]) with patch("models.rerank.rerank.requests.post") as mock_post: From 8ea8dedc8cd980ba886f6795e9928d24e7464132 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Mon, 7 Sep 2026 14:34:54 +0000 Subject: [PATCH 5/5] fix(openai_api_compatible): sanitize rerank response errors --- .../models/rerank/rerank.py | 8 ++-- .../tests/test_rerank_authorization.py | 48 ++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/models/openai_api_compatible/models/rerank/rerank.py b/models/openai_api_compatible/models/rerank/rerank.py index cd6b5d632..d2100d13b 100644 --- a/models/openai_api_compatible/models/rerank/rerank.py +++ b/models/openai_api_compatible/models/rerank/rerank.py @@ -181,8 +181,8 @@ def _invoke( raise InvokeServerUnavailableError( f"Rerank API request failed ({detail})" ) from None - except Exception as ex: - raise InvokeError(str(ex)) + except Exception: + raise InvokeError("Rerank API returned an invalid response") from None def _invoke_multimodal( self, @@ -315,8 +315,8 @@ def _invoke_multimodal( raise InvokeServerUnavailableError( f"Rerank API request failed ({detail})" ) from None - except Exception as ex: - raise InvokeError(str(ex)) + except Exception: + raise InvokeError("Rerank API returned an invalid response") from None def _validate_image_url(self, url: str) -> str: """ diff --git a/models/openai_api_compatible/tests/test_rerank_authorization.py b/models/openai_api_compatible/tests/test_rerank_authorization.py index a27574317..e634176a7 100644 --- a/models/openai_api_compatible/tests/test_rerank_authorization.py +++ b/models/openai_api_compatible/tests/test_rerank_authorization.py @@ -16,7 +16,7 @@ import pytest import requests -from dify_plugin.errors.model import InvokeServerUnavailableError +from dify_plugin.errors.model import InvokeError, InvokeServerUnavailableError from models.rerank.rerank import OpenAIRerankModel @@ -155,6 +155,52 @@ def test_http_error_exposes_only_status_code(caplog): assert "response-secret" not in error_output + caplog.text +@pytest.mark.parametrize("multimodal", [False, True]) +def test_invalid_response_does_not_expose_values(multimodal): + model = OpenAIRerankModel(model_schemas=[]) + response_secret = "response-secret" + response = MagicMock( + status_code=200, + json=lambda: {"results": [{"index": 0, "relevance_score": response_secret}]}, + ) + + with patch("models.rerank.rerank.requests.post", return_value=response): + with pytest.raises(InvokeError) as exc_info: + if multimodal: + from dify_plugin.entities.model.text_embedding import ( + MultiModalContent, + MultiModalContentType, + ) + + query = MultiModalContent( + content_type=MultiModalContentType.TEXT, + content="q", + ) + docs = [ + MultiModalContent( + content_type=MultiModalContentType.TEXT, + content="d1", + ) + ] + model._invoke_multimodal( + model="qwen3-vl-reranker", + credentials=_credentials(), + query=query, + docs=docs, + ) + else: + model._invoke( + model="bge-reranker-v2-m3", + credentials=_credentials(), + query="q", + docs=["d1"], + ) + + error_output = "".join(traceback.format_exception(exc_info.value)) + assert str(exc_info.value) == "Rerank API returned an invalid response" + assert response_secret not in error_output + + def test_rerank_logs_exclude_multimodal_content_and_image_urls(caplog): from dify_plugin.entities.model.text_embedding import ( MultiModalContent,