From 5b035fa6358903567c42de3af879330450217ed1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 08:34:54 +0000 Subject: [PATCH] fix(keenable): preserve upstream error text on failed search responses Non-2xx Keenable bodies were parsed as success envelopes and surfaced schema mismatch errors instead of the API message. Dispatch through get_error_class before validating the success shape, matching other POST search providers. Co-authored-by: Karl-Gustav Kallasmaa --- litellm/llms/keenable/search/transformation.py | 9 +++++++++ .../search/test_keenable_search_transformation.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/litellm/llms/keenable/search/transformation.py b/litellm/llms/keenable/search/transformation.py index 2a27c9e2c12..b6176e55b71 100644 --- a/litellm/llms/keenable/search/transformation.py +++ b/litellm/llms/keenable/search/transformation.py @@ -207,6 +207,15 @@ class KeenableSearchConfig(BaseSearchConfig): Records whether the keyless endpoint answered, so the cost calculator prices only keyed searches. Written unconditionally, so a caller-supplied value never sets its own cost. """ + # POST search handlers do not always call raise_for_status, so non-2xx bodies can arrive + # here and must surface Keenable's error envelope instead of a schema mismatch. + if not (200 <= raw_response.status_code < 300): + raise self.get_error_class( + error_message=raw_response.text, + status_code=raw_response.status_code, + headers=dict(raw_response.headers), + ) + try: parsed: Final = _KeenableSearchResponse.model_validate_json(raw_response.content) except ValidationError as e: diff --git a/tests/unit/llms/keenable/search/test_keenable_search_transformation.py b/tests/unit/llms/keenable/search/test_keenable_search_transformation.py index 665bb8e52b0..8eceb309104 100644 --- a/tests/unit/llms/keenable/search/test_keenable_search_transformation.py +++ b/tests/unit/llms/keenable/search/test_keenable_search_transformation.py @@ -241,6 +241,21 @@ def test_litellm_search_end_to_end(api_key, path: str, auth): assert response.results[0].snippet == "Test page text" +def test_non_2xx_response_surfaces_the_api_error_message(): + with pytest.raises(BaseLLMException, match="Public API hourly limit reached") as excinfo: + _config().transform_search_response( + _resp( + {"error": "Rate limit exceeded", "message": "Public API hourly limit reached"}, + status_code=429, + ), + logging_obj=Mock(optional_params={}), + ) + + assert excinfo.value.status_code == 429 + assert "set KEENABLE_API_KEY" in excinfo.value.message + assert "does not match" not in excinfo.value.message + + def test_litellm_search_surfaces_the_api_error_message(): # A non-2xx answer must reach the caller as Keenable's own message, never as a schema mismatch. request = httpx.Request("POST", f"{DEFAULT_ROOT}/search/public")