""" Calls Serper's /search endpoint to search Google. Serper API Reference: https://serper.dev """ from typing import Dict, List, Optional, TypedDict, Union import httpx from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.base_llm.search.transformation import ( BaseSearchConfig, SearchResponse, SearchResult, ) from litellm.secret_managers.main import get_secret_str class _SerperSearchRequestRequired(TypedDict): """Required fields for Serper Search API request.""" q: str # Required - search query class SerperSearchRequest(_SerperSearchRequestRequired, total=False): """ Serper Search API request format. Based on: https://serper.dev """ num: int # Optional - number of results to return, default 10 page: int # Optional - page number (default 1) gl: str # Optional - country/geolocation code (e.g., "us", "gb") hl: str # Optional - language code (e.g., "en", "de") location: str # Optional - specific location for search targeting autocorrect: bool # Optional - enable autocorrect (default True) tbs: str # Optional - time-based search filter (e.g., "qdr:h", "qdr:d", "qdr:w") class SerperSearchConfig(BaseSearchConfig): SERPER_API_BASE = "https://google.serper.dev" @staticmethod def ui_friendly_name() -> str: return "Serper" def validate_environment( self, headers: Dict, api_key: Optional[str] = None, api_base: Optional[str] = None, **kwargs, ) -> Dict: """ Validate environment and return headers. """ api_key = api_key or get_secret_str("SERPER_API_KEY") if not api_key: raise ValueError( "SERPER_API_KEY is not set. Set `SERPER_API_KEY` environment variable." ) headers["X-API-KEY"] = api_key headers["Content-Type"] = "application/json" return headers def get_complete_url( self, api_base: Optional[str], optional_params: dict, data: Optional[Union[Dict, List[Dict]]] = None, **kwargs, ) -> str: """ Get complete URL for Search endpoint. """ api_base = api_base or get_secret_str("SERPER_API_BASE") or self.SERPER_API_BASE api_base = api_base.rstrip("/") if not api_base.endswith("/search"): api_base = f"{api_base}/search" return api_base def transform_search_request( self, query: Union[str, List[str]], optional_params: dict, **kwargs, ) -> Dict: """ Transform Search request to Serper API format. Args: query: Search query (string or list of strings). Serper only supports single string queries. optional_params: Optional parameters for the request - max_results: Maximum number of search results -> maps to `num` - search_domain_filter: List of domains -> appended as site: clauses to `q` - country: Country code filter (e.g., 'US', 'GB') -> maps to `gl` (lowercased) Returns: Dict with typed request data following SerperSearchRequest spec """ if isinstance(query, list): query = " ".join(query) request_data: SerperSearchRequest = { "q": query, } if "max_results" in optional_params: request_data["num"] = optional_params["max_results"] if "country" in optional_params: request_data["gl"] = optional_params["country"].lower() if "search_domain_filter" in optional_params: domains = optional_params["search_domain_filter"] if isinstance(domains, list) and len(domains) > 0: domain_clauses = " OR ".join(f"site:{d}" for d in domains) request_data["q"] = f"({request_data['q']}) ({domain_clauses})" # Convert to dict before dynamic key assignments result_data = 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 return result_data def transform_search_response( self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, **kwargs, ) -> SearchResponse: """ Transform Serper API response to LiteLLM unified SearchResponse format. Serper -> LiteLLM mappings: - organic[].title -> SearchResult.title - organic[].link -> SearchResult.url - organic[].snippet -> SearchResult.snippet - organic[].date -> SearchResult.date (optional, not always present) Args: raw_response: Raw httpx response from Serper API logging_obj: Logging object for tracking Returns: SearchResponse with standardized format """ response_json = raw_response.json() results = [] for result in response_json.get("organic", []): search_result = SearchResult( title=result.get("title", ""), url=result.get("link", ""), snippet=result.get("snippet", ""), date=result.get("date"), last_updated=None, ) results.append(search_result) return SearchResponse( results=results, object="search", )