From bb863a9440bc199ff415afc8d5491d60bdad39a7 Mon Sep 17 00:00:00 2001 From: HenryHYeary Date: Tue, 8 Sep 2026 21:16:57 -0500 Subject: [PATCH] feat(search): begin unified start_date/end_date support for search providers --- litellm/llms/linkup/search/transformation.py | 31 +++++++---- litellm/search/main.py | 20 +++++++ .../test_linkup_search_transformation.py | 52 +++++++++++++++++++ .../test_nimble_search_transformation.py | 8 +++ 4 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 tests/test_litellm/llms/linkup/search/test_linkup_search_transformation.py diff --git a/litellm/llms/linkup/search/transformation.py b/litellm/llms/linkup/search/transformation.py index 8ecd5a24c08..4a99ad72d9e 100644 --- a/litellm/llms/linkup/search/transformation.py +++ b/litellm/llms/linkup/search/transformation.py @@ -104,6 +104,8 @@ class LinkupSearchConfig(BaseSearchConfig): - max_results -> maxResults - search_domain_filter -> includeDomains - country -> (not directly supported) + - start_date -> fromDate + - end_date -> toDate - max_tokens_per_page -> (not applicable) All other Linkup-specific parameters are passed through as-is. @@ -119,26 +121,33 @@ class LinkupSearchConfig(BaseSearchConfig): # Linkup only supports single string queries, join with spaces query = " ".join(query) + # Copy for passthrough data (Done this way to avoid having to change / add to Perplexity unified spec parameters) + remaining = dict(optional_params) + request_data: Final[LinkupSearchRequest] = { "q": query, - "depth": optional_params.get("depth", "standard"), - "outputType": optional_params.get("outputType", "searchResults"), + "depth": remaining.pop("depth", "standard"), + "outputType": remaining.pop("outputType", "searchResults"), } # Transform Perplexity unified spec parameters to Linkup format - if "max_results" in optional_params: - request_data["maxResults"] = optional_params["max_results"] + if "max_results" in remaining: + request_data["maxResults"] = remaining.pop("max_results") - if "search_domain_filter" in optional_params: - request_data["includeDomains"] = optional_params["search_domain_filter"] + if "search_domain_filter" in remaining: + request_data["includeDomains"] = remaining.pop("search_domain_filter") + + if "start_date" in remaining: + request_data["fromDate"] = remaining.pop("start_date") + + if "end_date" in remaining: + request_data["toDate"] = remaining.pop("end_date") # Convert to dict before dynamic key assignments result_data: Final = dict(request_data) - # pass through all other parameters as-is - for param, value in optional_params.items(): - if param not in self.get_supported_perplexity_optional_params() and param not in result_data: - result_data[param] = value + # Pass through any unhandled data + result_data.update(remaining) return result_data @@ -200,4 +209,4 @@ class LinkupSearchConfig(BaseSearchConfig): return SearchResponse( results=results, object="search", - ) + ) \ No newline at end of file diff --git a/litellm/search/main.py b/litellm/search/main.py index b2dd51799a1..20fa6046553 100644 --- a/litellm/search/main.py +++ b/litellm/search/main.py @@ -29,6 +29,8 @@ def _build_search_optional_params( search_domain_filter: list[str] | None = None, max_tokens_per_page: int | None = None, country: str | None = None, + start_date: str | None = None, + end_date: str | None = None, ) -> dict[str, Any]: """ Helper function to build optional_params dict from Perplexity Search API parameters. @@ -38,6 +40,8 @@ def _build_search_optional_params( search_domain_filter: List of domains to filter (max 20) max_tokens_per_page: Max tokens per page country: Country code filter + start_date: Start date for results (YYYY-MM-DD) + end_date: End date for results (YYYY-MM-DD) Returns: Dict with non-None optional parameters @@ -52,6 +56,10 @@ def _build_search_optional_params( optional_params["max_tokens_per_page"] = max_tokens_per_page if country is not None: optional_params["country"] = country + if start_date is not None: + optional_params["start_date"] = start_date + if end_date is not None: + optional_params["end_date"] = end_date return optional_params @@ -64,6 +72,8 @@ async def asearch( search_domain_filter: list[str] | None = None, max_tokens_per_page: int | None = None, country: str | None = None, + start_date: str | None = None, + end_date: str | None = None, api_key: str | None = None, api_base: str | None = None, timeout: float | httpx.Timeout | None = None, @@ -80,6 +90,8 @@ async def asearch( search_domain_filter: Optional list of domains to filter (max 20) max_tokens_per_page: Optional max tokens per page, default 1024 country: Optional country code filter (e.g., 'US', 'GB', 'DE') + start_date: Optional start date for results (YYYY-MM-DD) + end_date: Optional end date for results (YYYY-MM-DD) api_key: Optional API key api_base: Optional API base URL timeout: Optional timeout @@ -128,6 +140,8 @@ async def asearch( search_domain_filter=search_domain_filter, max_tokens_per_page=max_tokens_per_page, country=country, + start_date=start_date, + end_date=end_date, api_key=api_key, api_base=api_base, timeout=timeout, @@ -167,6 +181,8 @@ def search( search_domain_filter: list[str] | None = None, max_tokens_per_page: int | None = None, country: str | None = None, + start_date: str | None = None, + end_date: str | None = None, api_key: str | None = None, api_base: str | None = None, timeout: float | httpx.Timeout | None = None, @@ -183,6 +199,8 @@ def search( search_domain_filter: Optional list of domains to filter (max 20) max_tokens_per_page: Optional max tokens per page, default 1024 country: Optional country code filter (e.g., 'US', 'GB', 'DE') + start_date: Optional start date for results (YYYY-MM-DD) + end_date: Optional end date for results (YYYY-MM-DD) api_key: Optional API key api_base: Optional API base URL timeout: Optional timeout @@ -255,6 +273,8 @@ def search( search_domain_filter=search_domain_filter, max_tokens_per_page=max_tokens_per_page, country=country, + start_date=start_date, + end_date=end_date, ) # Filter out internal LiteLLM parameters from kwargs diff --git a/tests/test_litellm/llms/linkup/search/test_linkup_search_transformation.py b/tests/test_litellm/llms/linkup/search/test_linkup_search_transformation.py new file mode 100644 index 00000000000..7cc86303826 --- /dev/null +++ b/tests/test_litellm/llms/linkup/search/test_linkup_search_transformation.py @@ -0,0 +1,52 @@ +import json +from unittest.mock import Mock + +import pytest + +from litellm.llms.linkup.search.transformation import LinkupSearchConfig + + +def _config() -> LinkupSearchConfig: + return LinkupSearchConfig() + + +def test_transform_search_request_maps_start_date_to_from_date(): + data = _config().transform_search_request("q", {"start_date": "1999-03-20"}) + assert data["fromDate"] == "1999-03-20" + assert "start_date" not in data + + +def test_transform_search_request_maps_end_date_to_to_date(): + data = _config().transform_search_request("q", {"end_date": "1999-04-20"}) + assert data["toDate"] == "1999-04-20" + assert "end_date" not in data + + +def test_transform_search_request_date_range_together(): + data = _config().transform_search_request( + "q", {"start_date": "1999-03-20", "end_date": "1999-04-20"} + ) + assert data["fromDate"] == "1999-03-20" + assert data["toDate"] == "1999-04-20" + + +def test_transform_search_request_without_dates_omits_both(): + data = _config().transform_search_request("q", {"max_results": 5}) + assert "fromDate" not in data + assert "toDate" not in data + + +def test_transform_search_request_passes_through_unhandled_kwargs(): + """A param this function doesn't explicitly handle should still reach Linkup unchanged.""" + data = _config().transform_search_request("q", {"includeImages": True}) + assert data["includeImages"] is True + + +def test_transform_search_request_joins_list_query(): + assert _config().transform_search_request(["foo", "bar"], {})["q"] == "foo bar" + + +def test_transform_search_request_defaults_depth_and_output_type(): + data = _config().transform_search_request("q", {}) + assert data["depth"] == "standard" + assert data["outputType"] == "searchResults" \ No newline at end of file diff --git a/tests/test_litellm/llms/nimble/search/test_nimble_search_transformation.py b/tests/test_litellm/llms/nimble/search/test_nimble_search_transformation.py index d6292c9cf3e..dc1e8ab5625 100644 --- a/tests/test_litellm/llms/nimble/search/test_nimble_search_transformation.py +++ b/tests/test_litellm/llms/nimble/search/test_nimble_search_transformation.py @@ -100,6 +100,14 @@ def test_transform_search_request_max_results_is_not_clamped(): def test_transform_search_request_uppercases_country(): assert _config().transform_search_request("q", {"country": "us"})["country"] == "US" +def test_transform_search_request_date_range_passes_through_native_names(): + """Nimble already uses the unified spec's own field names for date filtering.""" + data = _config().transform_search_request( + "q", {"start_date": "1999-03-20", "end_date": "1999-04-20"} + ) + assert data["start_date"] == "1999-03-20" + assert data["end_date"] == "1999-04-20" + def test_transform_search_request_drops_max_tokens_per_page(): assert "max_tokens_per_page" not in _config().transform_search_request("q", {"max_tokens_per_page": 1024})