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 <KGKallasmaa@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-11-05 08:34:54 +00:00
parent 48317988a4
commit 5b035fa635
No known key found for this signature in database
2 changed files with 24 additions and 0 deletions

View file

@ -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:

View file

@ -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")