fix(exa): fall back to highlights/summary when text is missing (#42213)

* fix(exa): fall back to highlights/summary when text is missing (#36905)

* test: type exa search transformation test helpers

* test(exa): drop type: ignore in test helper and trim redundant comments

* test(exa): move snippet fallback test to tests/unit and parametrize it

Uses a real httpx.Response instead of a Mock and annotates snippet as Final.

* fix(exa): inline snippet fallback since Final is not allowed inside a loop

---------

Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
This commit is contained in:
Waqas Ahmed 2026-10-02 14:15:15 -07:00 • committed by GitHub
parent ac59ec6824
commit 4a847ac936
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 39 additions and 1 deletions

View file

@ -165,6 +165,8 @@ class ExaAISearchConfig(BaseSearchConfig):
- results[].title → SearchResult.title
- results[].url → SearchResult.url
- results[].text → SearchResult.snippet
- results[].highlights → SearchResult.snippet (fallback when "text" is absent)
- results[].summary → SearchResult.snippet (fallback when "text" and "highlights" are absent)
- results[].publishedDate → SearchResult.date
- No last_updated field in Exa AI response (set to None)
@ -183,7 +185,10 @@ class ExaAISearchConfig(BaseSearchConfig):
search_result = SearchResult(
title=result.get("title", ""),
url=result.get("url", ""),
snippet=result.get("text", ""), # Exa AI uses "text" for content
snippet=result.get("text")
or "\n\n".join(result.get("highlights") or [])
or result.get("summary")
or "",
date=result.get("publishedDate"), # ISO 8601 datetime string
last_updated=None, # Exa AI doesn't provide last_updated in response
)

View file

View file

@ -0,0 +1,33 @@
from typing import Final
from unittest.mock import Mock
import httpx
import pytest
from litellm.llms.exa_ai.search.transformation import ExaAISearchConfig
@pytest.mark.parametrize(
("content_fields", "expected_snippet"),
[
({"text": "full text"}, "full text"),
({"highlights": ["first highlight", "second highlight"]}, "first highlight\n\nsecond highlight"),
({"summary": "a summary"}, "a summary"),
({"text": "full text", "highlights": ["a highlight"], "summary": "a summary"}, "full text"),
({"highlights": ["a highlight"], "summary": "a summary"}, "a highlight"),
({"text": "", "highlights": ["a highlight"]}, "a highlight"),
({"highlights": [], "summary": "a summary"}, "a summary"),
({}, ""),
],
)
def test_transform_search_response_snippet_falls_back_through_content_modes(
content_fields: dict[str, str | list[str]], expected_snippet: str
):
raw_response: Final = httpx.Response(
200,
json={"results": [{"title": "Title", "url": "https://example.com", **content_fields}]},
)
response: Final = ExaAISearchConfig().transform_search_response(raw_response, logging_obj=Mock())
assert response.results[0].snippet == expected_snippet