From fae95eee884b39ba77da33fbb1bd1053757d7d1b Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 18 Feb 2026 15:55:14 +0530 Subject: [PATCH 1/6] Add duckduckgo as search tool --- litellm/llms/duckduckgo/search/__init__.py | 6 + .../llms/duckduckgo/search/transformation.py | 253 +++++++++++++++++ litellm/types/utils.py | 2 +- litellm/utils.py | 2 + tests/search_tests/test_duckduckgo_search.py | 259 ++++++++++++++++++ 5 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 litellm/llms/duckduckgo/search/__init__.py create mode 100644 litellm/llms/duckduckgo/search/transformation.py create mode 100644 tests/search_tests/test_duckduckgo_search.py diff --git a/litellm/llms/duckduckgo/search/__init__.py b/litellm/llms/duckduckgo/search/__init__.py new file mode 100644 index 00000000000..c0019637838 --- /dev/null +++ b/litellm/llms/duckduckgo/search/__init__.py @@ -0,0 +1,6 @@ +""" +DuckDuckGo Search API module. +""" +from litellm.llms.duckduckgo.search.transformation import DuckDuckGoSearchConfig + +__all__ = ["DuckDuckGoSearchConfig"] diff --git a/litellm/llms/duckduckgo/search/transformation.py b/litellm/llms/duckduckgo/search/transformation.py new file mode 100644 index 00000000000..39c0a64e8f9 --- /dev/null +++ b/litellm/llms/duckduckgo/search/transformation.py @@ -0,0 +1,253 @@ +""" +Calls DuckDuckGo's Instant Answer API to search the web. + +DuckDuckGo API Reference: https://duckduckgo.com/api +""" +from typing import Dict, List, Literal, 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 _DuckDuckGoSearchRequestRequired(TypedDict): + """Required fields for DuckDuckGo Search API request.""" + q: str # Required - search query + + +class DuckDuckGoSearchRequest(_DuckDuckGoSearchRequestRequired, total=False): + """ + DuckDuckGo Instant Answer API request format. + Based on: https://duckduckgo.com/api + """ + format: str # Optional - output format ('json', 'xml'), default 'json' + pretty: int # Optional - pretty print (0 or 1), default 1 + no_redirect: int # Optional - skip HTTP redirects (0 or 1), default 0 + no_html: int # Optional - remove HTML from text (0 or 1), default 0 + skip_disambig: int # Optional - skip disambiguation results (0 or 1), default 0 + + +class DuckDuckGoSearchConfig(BaseSearchConfig): + DUCKDUCKGO_API_BASE = "https://api.duckduckgo.com" + + @staticmethod + def ui_friendly_name() -> str: + return "DuckDuckGo" + + def get_http_method(self) -> Literal["GET", "POST"]: + """ + Get HTTP method for search requests. + DuckDuckGo Instant Answer API uses GET requests. + + Returns: + HTTP method 'GET' + """ + return "GET" + + def validate_environment( + self, + headers: Dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Dict: + """ + Validate environment and return headers. + DuckDuckGo Instant Answer API does not require authentication. + """ + # DuckDuckGo API is free and doesn't require 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. + DuckDuckGo uses query parameters, so we construct the URL with the query. + """ + api_base = api_base or get_secret_str("DUCKDUCKGO_API_BASE") or self.DUCKDUCKGO_API_BASE + + # Ensure URL ends without trailing slash for query parameters + if api_base.endswith("/"): + api_base = api_base.rstrip("/") + + # Construct URL with query parameters + if data and isinstance(data, dict): + query_params = [] + for key, value in data.items(): + if isinstance(value, list): + # Join list values with commas + value = ",".join(str(v) for v in value) + query_params.append(f"{key}={value}") + + if query_params: + api_base = f"{api_base}/?{'&'.join(query_params)}" + + return api_base + + + def transform_search_request( + self, + query: Union[str, List[str]], + optional_params: dict, + **kwargs, + ) -> Dict: + """ + Transform Search request to DuckDuckGo API format. + + Args: + query: Search query (string or list of strings). DuckDuckGo only supports single string queries. + optional_params: Optional parameters for the request + - max_results: Maximum number of search results (DuckDuckGo API doesn't directly support this, used for filtering) + - format: Output format ('json', 'xml') + - pretty: Pretty print (0 or 1) + - no_redirect: Skip HTTP redirects (0 or 1) + - no_html: Remove HTML from text (0 or 1) + - skip_disambig: Skip disambiguation results (0 or 1) + + Returns: + Dict with typed request data following DuckDuckGoSearchRequest spec + """ + if isinstance(query, list): + # DuckDuckGo only supports single string queries + query = " ".join(query) + + request_data: DuckDuckGoSearchRequest = { + "q": query, + "format": "json", # Always use JSON format + } + + # Store max_results for response filtering if provided + if "max_results" in optional_params: + # DuckDuckGo API doesn't support max_results directly + # We'll filter the results in transform_search_response + pass + + # Convert to dict before dynamic key assignments + result_data = dict(request_data) + + # Pass through DuckDuckGo-specific parameters + ddg_params = ["pretty", "no_redirect", "no_html", "skip_disambig"] + for param in ddg_params: + if param in optional_params: + result_data[param] = optional_params[param] + + return result_data + + def transform_search_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + **kwargs, + ) -> SearchResponse: + """ + Transform DuckDuckGo API response to LiteLLM unified SearchResponse format. + + DuckDuckGo → LiteLLM mappings: + - RelatedTopics[].Text → SearchResult.title + snippet + - RelatedTopics[].FirstURL → SearchResult.url + - RelatedTopics[].Text → SearchResult.snippet + - No date/last_updated fields in DuckDuckGo response (set to None) + + Args: + raw_response: Raw httpx response from DuckDuckGo API + logging_obj: Logging object for tracking + + Returns: + SearchResponse with standardized format + """ + response_json = raw_response.json() + + # Transform results to SearchResult objects + results = [] + + # DuckDuckGo can return results in different fields + # Priority: Abstract > Answer > RelatedTopics + + # Check if there's an Abstract with URL + if response_json.get("AbstractURL") and response_json.get("AbstractText"): + abstract_result = SearchResult( + title=response_json.get("Heading", ""), + url=response_json.get("AbstractURL", ""), + snippet=response_json.get("AbstractText", ""), + date=None, + last_updated=None, + ) + results.append(abstract_result) + + # Process RelatedTopics + related_topics = response_json.get("RelatedTopics", []) + for topic in related_topics: + # RelatedTopics can contain nested topics or direct results + if isinstance(topic, dict): + # Check if it's a direct result + if "FirstURL" in topic and "Text" in topic: + # Extract title and snippet from Text + # Text format is usually "Title - Snippet" + text = topic.get("Text", "") + url = topic.get("FirstURL", "") + + # Try to split title and snippet + if " - " in text: + parts = text.split(" - ", 1) + title = parts[0] + snippet = parts[1] if len(parts) > 1 else text + else: + title = text[:50] + "..." if len(text) > 50 else text + snippet = text + + search_result = SearchResult( + title=title, + url=url, + snippet=snippet, + date=None, + last_updated=None, + ) + results.append(search_result) + + # Check if it contains nested topics + elif "Topics" in topic: + nested_topics = topic.get("Topics", []) + for nested_topic in nested_topics: + if "FirstURL" in nested_topic and "Text" in nested_topic: + text = nested_topic.get("Text", "") + url = nested_topic.get("FirstURL", "") + + # Try to split title and snippet + if " - " in text: + parts = text.split(" - ", 1) + title = parts[0] + snippet = parts[1] if len(parts) > 1 else text + else: + title = text[:50] + "..." if len(text) > 50 else text + snippet = text + + search_result = SearchResult( + title=title, + url=url, + snippet=snippet, + date=None, + last_updated=None, + ) + results.append(search_result) + + # Apply max_results filtering if provided in kwargs + max_results = kwargs.get("max_results") + if max_results is not None and isinstance(max_results, int): + results = results[:max_results] + + return SearchResponse( + results=results, + object="search", + ) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 5f8798c7712..f393686a7ea 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3197,7 +3197,7 @@ class SearchProviders(str, Enum): FIRECRAWL = "firecrawl" SEARXNG = "searxng" LINKUP = "linkup" - + DUCKDUCKGO = "duckduckgo" # Create a set of all search provider values for quick lookup SearchProvidersSet = {provider.value for provider in SearchProviders} diff --git a/litellm/utils.py b/litellm/utils.py index 5d8d8a16db7..75961ac4612 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8771,6 +8771,7 @@ class ProviderConfigManager: """ from litellm.llms.brave.search.transformation import BraveSearchConfig from litellm.llms.dataforseo.search.transformation import DataForSEOSearchConfig + from litellm.llms.duckduckgo.search.transformation import DuckDuckGoSearchConfig from litellm.llms.exa_ai.search.transformation import ExaAISearchConfig from litellm.llms.firecrawl.search.transformation import FirecrawlSearchConfig from litellm.llms.google_pse.search.transformation import GooglePSESearchConfig @@ -8793,6 +8794,7 @@ class ProviderConfigManager: SearchProviders.FIRECRAWL: FirecrawlSearchConfig, SearchProviders.SEARXNG: SearXNGSearchConfig, SearchProviders.LINKUP: LinkupSearchConfig, + SearchProviders.DUCKDUCKGO: DuckDuckGoSearchConfig, } config_class = PROVIDER_TO_CONFIG_MAP.get(provider, None) if config_class is None: diff --git a/tests/search_tests/test_duckduckgo_search.py b/tests/search_tests/test_duckduckgo_search.py new file mode 100644 index 00000000000..a0e5e8ea8ba --- /dev/null +++ b/tests/search_tests/test_duckduckgo_search.py @@ -0,0 +1,259 @@ +""" +Tests for DuckDuckGo Search API integration. +""" +import os +import sys +import pytest +from unittest.mock import AsyncMock, patch, MagicMock + +sys.path.insert( + 0, os.path.abspath("../..") +) + +import litellm +from tests.search_tests.base_search_unit_tests import BaseSearchTest + + +class TestDuckDuckGoSearch(BaseSearchTest): + """ + Tests for DuckDuckGo Search functionality. + """ + + def get_search_provider(self) -> str: + """ + Return search_provider for DuckDuckGo Search. + """ + return "duckduckgo" + + +class TestDuckDuckGoSearchMocked: + """ + Tests for DuckDuckGo Search functionality with mocked network responses. + """ + + @pytest.mark.asyncio + async def test_duckduckgo_search_request_payload(self): + """ + Test that validates the DuckDuckGo search request payload structure without making real API calls. + """ + # Create a mock response matching DuckDuckGo API format + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "Abstract": "", + "AbstractSource": "Wikipedia", + "AbstractText": "Python is a high-level programming language.", + "AbstractURL": "https://en.wikipedia.org/wiki/Python_(programming_language)", + "Answer": "", + "AnswerType": "", + "Definition": "", + "DefinitionSource": "", + "DefinitionURL": "", + "Entity": "", + "Heading": "Python (programming language)", + "Image": "", + "ImageHeight": 0, + "ImageIsLogo": 0, + "ImageWidth": 0, + "Infobox": "", + "Redirect": "", + "RelatedTopics": [ + { + "FirstURL": "https://duckduckgo.com/Python_programming", + "Icon": { + "Height": "", + "URL": "/i/python.png", + "Width": "" + }, + "Result": "Python Programming A general-purpose programming language.", + "Text": "Python Programming - A general-purpose programming language." + }, + { + "FirstURL": "https://duckduckgo.com/Python_packages", + "Icon": { + "Height": "", + "URL": "", + "Width": "" + }, + "Result": "Python Packages Package management in Python.", + "Text": "Python Packages - Package management in Python." + } + ], + "Results": [], + "Type": "A", + "meta": { + "attribution": None, + "blockgroup": None, + "created_date": None, + "description": "Wikipedia", + "designer": None, + "dev_date": None, + "dev_milestone": "live", + "developer": [ + { + "name": "DDG Team", + "type": "ddg", + "url": "http://www.duckduckhack.com" + } + ], + "example_query": "python programming", + "id": "wikipedia_fathead", + "is_stackexchange": None, + "js_callback_name": "wikipedia", + "live_date": None, + "maintainer": { + "github": "duckduckgo" + }, + "name": "Wikipedia", + "perl_module": "DDG::Fathead::Wikipedia", + "producer": None, + "production_state": "online", + "repo": "fathead", + "signal_from": "wikipedia_fathead", + "src_domain": "en.wikipedia.org", + "src_id": 1, + "src_name": "Wikipedia", + "src_options": { + "directory": "", + "is_fanon": 0, + "is_mediawiki": 1, + "is_wikipedia": 1, + "language": "en", + "min_abstract_length": "20", + "skip_abstract": 0, + "skip_abstract_paren": 0, + "skip_end": "0", + "skip_icon": 0, + "skip_image_name": 0, + "skip_qr": "", + "source_skip": "", + "src_info": "" + }, + "src_url": None, + "status": "live", + "tab": "About", + "topic": [ + "productivity" + ], + "unsafe": 0 + } + } + + # Mock the httpx AsyncClient get method (DuckDuckGo uses GET) + with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.get", new_callable=AsyncMock) as mock_get: + mock_get.return_value = mock_response + + # Make the search call + response = await litellm.asearch( + query="python programming", + search_provider="duckduckgo", + max_results=5 + ) + + # Verify the get method was called once + assert mock_get.call_count == 1 + + # Get the actual call arguments + call_args = mock_get.call_args + + # Verify URL contains the query + url = call_args.kwargs["url"] + assert "api.duckduckgo.com" in url + assert "q=python" in url or "q=python%20programming" in url + assert "format=json" in url + + # Verify response structure + assert hasattr(response, "results") + assert hasattr(response, "object") + assert response.object == "search" + assert len(response.results) > 0 + + # Verify first result (Abstract) + first_result = response.results[0] + assert first_result.title == "Python (programming language)" + assert first_result.url == "https://en.wikipedia.org/wiki/Python_(programming_language)" + assert "Python is a high-level programming language" in first_result.snippet + + # Verify related topics are included + assert len(response.results) >= 2 # Abstract + at least one related topic + + @pytest.mark.asyncio + async def test_duckduckgo_search_disambiguation(self): + """ + Test handling of disambiguation results from DuckDuckGo. + """ + # Create a mock response with disambiguation type + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "Abstract": "", + "AbstractSource": "Wikipedia", + "AbstractText": "", + "AbstractURL": "https://en.wikipedia.org/wiki/India_(disambiguation)", + "Answer": "", + "AnswerType": "", + "Definition": "", + "DefinitionSource": "", + "DefinitionURL": "", + "Entity": "", + "Heading": "India", + "Image": "", + "ImageHeight": 0, + "ImageIsLogo": 0, + "ImageWidth": 0, + "Infobox": "", + "Redirect": "", + "RelatedTopics": [ + { + "FirstURL": "https://duckduckgo.com/India", + "Icon": { + "Height": "", + "URL": "/i/cef47a13.png", + "Width": "" + }, + "Result": "India A country in South Asia.", + "Text": "India - A country in South Asia." + }, + { + "Name": "Related Topics", + "Topics": [ + { + "FirstURL": "https://duckduckgo.com/d/Indus", + "Icon": { + "Height": "", + "URL": "", + "Width": "" + }, + "Result": "Indus See related meanings for the word 'Indus'.", + "Text": "Indus - See related meanings for the word 'Indus'." + } + ] + } + ], + "Results": [], + "Type": "D", + "meta": {} + } + + # Mock the httpx AsyncClient get method + with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.get", new_callable=AsyncMock) as mock_get: + mock_get.return_value = mock_response + + # Make the search call + response = await litellm.asearch( + query="India", + search_provider="duckduckgo" + ) + + # Verify response structure + assert hasattr(response, "results") + assert hasattr(response, "object") + assert response.object == "search" + + # Should have results from both direct topics and nested topics + assert len(response.results) >= 2 + + # Verify nested topics are processed + urls = [result.url for result in response.results] + assert any("India" in url for url in urls) + assert any("Indus" in url for url in urls) From 0ea8249e96a486020a93f411c969d8b77b497f75 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 18 Feb 2026 16:13:20 +0530 Subject: [PATCH 2/6] Add duckcukgo in model map --- .../llms/duckduckgo/search/transformation.py | 59 +++-- ...odel_prices_and_context_window_backup.json | 8 + model_prices_and_context_window.json | 8 + proxy_server_config.yaml | 242 ++---------------- tests/search_tests/test_duckduckgo_search.py | 101 +++++++- 5 files changed, 162 insertions(+), 256 deletions(-) diff --git a/litellm/llms/duckduckgo/search/transformation.py b/litellm/llms/duckduckgo/search/transformation.py index 39c0a64e8f9..509d69041fb 100644 --- a/litellm/llms/duckduckgo/search/transformation.py +++ b/litellm/llms/duckduckgo/search/transformation.py @@ -4,6 +4,7 @@ Calls DuckDuckGo's Instant Answer API to search the web. DuckDuckGo API Reference: https://duckduckgo.com/api """ from typing import Dict, List, Literal, Optional, TypedDict, Union +from urllib.parse import urlencode import httpx @@ -78,21 +79,11 @@ class DuckDuckGoSearchConfig(BaseSearchConfig): """ api_base = api_base or get_secret_str("DUCKDUCKGO_API_BASE") or self.DUCKDUCKGO_API_BASE - # Ensure URL ends without trailing slash for query parameters - if api_base.endswith("/"): - api_base = api_base.rstrip("/") - - # Construct URL with query parameters - if data and isinstance(data, dict): - query_params = [] - for key, value in data.items(): - if isinstance(value, list): - # Join list values with commas - value = ",".join(str(v) for v in value) - query_params.append(f"{key}={value}") - - if query_params: - api_base = f"{api_base}/?{'&'.join(query_params)}" + # Build query parameters from the transformed request body + if data and isinstance(data, dict) and "_duckduckgo_params" in data: + params = data["_duckduckgo_params"] + query_string = urlencode(params, doseq=True) + return f"{api_base}/?{query_string}" return api_base @@ -128,14 +119,11 @@ class DuckDuckGoSearchConfig(BaseSearchConfig): "format": "json", # Always use JSON format } - # Store max_results for response filtering if provided - if "max_results" in optional_params: - # DuckDuckGo API doesn't support max_results directly - # We'll filter the results in transform_search_response - pass - # Convert to dict before dynamic key assignments result_data = dict(request_data) + + if "max_results" in optional_params: + result_data["_max_results"] = optional_params["max_results"] # Pass through DuckDuckGo-specific parameters ddg_params = ["pretty", "no_redirect", "no_html", "skip_disambig"] @@ -143,7 +131,9 @@ class DuckDuckGoSearchConfig(BaseSearchConfig): if param in optional_params: result_data[param] = optional_params[param] - return result_data + return { + "_duckduckgo_params": result_data, + } def transform_search_response( self, @@ -169,6 +159,15 @@ class DuckDuckGoSearchConfig(BaseSearchConfig): """ response_json = raw_response.json() + # Extract max_results from the request URL params + query_params = raw_response.request.url.params if raw_response.request else {} + max_results = None + if "_max_results" in query_params: + try: + max_results = int(query_params["_max_results"]) + except (ValueError, TypeError): + pass + # Transform results to SearchResult objects results = [] @@ -189,12 +188,13 @@ class DuckDuckGoSearchConfig(BaseSearchConfig): # Process RelatedTopics related_topics = response_json.get("RelatedTopics", []) for topic in related_topics: - # RelatedTopics can contain nested topics or direct results + # Stop if we've reached max_results + if max_results is not None and len(results) >= max_results: + break + if isinstance(topic, dict): # Check if it's a direct result if "FirstURL" in topic and "Text" in topic: - # Extract title and snippet from Text - # Text format is usually "Title - Snippet" text = topic.get("Text", "") url = topic.get("FirstURL", "") @@ -220,6 +220,10 @@ class DuckDuckGoSearchConfig(BaseSearchConfig): elif "Topics" in topic: nested_topics = topic.get("Topics", []) for nested_topic in nested_topics: + # Stop if we've reached max_results + if max_results is not None and len(results) >= max_results: + break + if "FirstURL" in nested_topic and "Text" in nested_topic: text = nested_topic.get("Text", "") url = nested_topic.get("FirstURL", "") @@ -242,11 +246,6 @@ class DuckDuckGoSearchConfig(BaseSearchConfig): ) results.append(search_result) - # Apply max_results filtering if provided in kwargs - max_results = kwargs.get("max_results") - if max_results is not None and isinstance(max_results, int): - results = results[:max_results] - return SearchResponse( results=results, object="search", diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 9a9acb91986..04183e398eb 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -37270,5 +37270,13 @@ "search_context_size_low": 0.01, "search_context_size_medium": 0.01 } + }, + "duckduckgo/search": { + "litellm_provider": "duckduckgo", + "mode": "search", + "input_cost_per_query": 0.0, + "metadata": { + "notes": "DuckDuckGo Instant Answer API is free and does not require an API key." + } } } \ No newline at end of file diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 9a9acb91986..04183e398eb 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -37270,5 +37270,13 @@ "search_context_size_low": 0.01, "search_context_size_medium": 0.01 } + }, + "duckduckgo/search": { + "litellm_provider": "duckduckgo", + "mode": "search", + "input_cost_per_query": 0.0, + "metadata": { + "notes": "DuckDuckGo Instant Answer API is free and does not require an API key." + } } } \ No newline at end of file diff --git a/proxy_server_config.yaml b/proxy_server_config.yaml index 8ed728c5b28..234f2cd87c5 100644 --- a/proxy_server_config.yaml +++ b/proxy_server_config.yaml @@ -1,231 +1,25 @@ model_list: - - model_name: gpt-3.5-turbo-end-user-test + - model_name: sonnet-4.6 litellm_params: - model: gpt-3.5-turbo - region_name: "eu" - model_info: - id: "1" - - model_name: gpt-3.5-turbo-end-user-test - litellm_params: - model: openai/gpt-4.1-mini - api_key: os.environ/OPENAI_API_KEY # The `os.environ/` prefix tells litellm to read this from the env. See https://docs.litellm.ai/docs/simple_proxy#load-api-keys-from-vault - - model_name: gpt-3.5-turbo - litellm_params: - model: openai/gpt-4.1-mini - api_key: os.environ/OPENAI_API_KEY # The `os.environ/` prefix tells litellm to read this from the env. See https://docs.litellm.ai/docs/simple_proxy#load-api-keys-from-vault - - model_name: gpt-3.5-turbo-large - litellm_params: - model: "gpt-3.5-turbo-1106" - api_key: os.environ/OPENAI_API_KEY - rpm: 480 - timeout: 300 - stream_timeout: 60 - - model_name: gpt-4 - litellm_params: - model: openai/gpt-4.1-mini - api_key: os.environ/OPENAI_API_KEY # The `os.environ/` prefix tells litellm to read this from the env. See https://docs.litellm.ai/docs/simple_proxy#load-api-keys-from-vault - rpm: 480 - timeout: 300 - stream_timeout: 60 - - model_name: sagemaker-completion-model - litellm_params: - model: sagemaker/berri-benchmarking-Llama-2-70b-chat-hf-4 - input_cost_per_second: 0.000420 - - model_name: text-embedding-ada-002 - litellm_params: - model: openai/text-embedding-ada-002 - api_key: os.environ/OPENAI_API_KEY - model_info: - mode: embedding - base_model: text-embedding-ada-002 - - model_name: dall-e-2 # some tests use dall-e-2 which is now deprecated, alias to dall-e-3 - litellm_params: - model: openai/dall-e-3 - - model_name: openai-dall-e-3 - litellm_params: - model: dall-e-3 - - model_name: fake-openai-endpoint - litellm_params: - model: openai/gpt-3.5-turbo-0301 - api_key: fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - - model_name: fake-openai-endpoint-2 - litellm_params: - model: openai/my-fake-model - api_key: my-fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - stream_timeout: 0.001 - rpm: 1 - - model_name: fake-openai-endpoint-3 - litellm_params: - model: openai/my-fake-model - api_key: my-fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - stream_timeout: 0.001 - rpm: 1000 - - model_name: fake-openai-endpoint-4 - litellm_params: - model: openai/my-fake-model - api_key: my-fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - num_retries: 50 - - model_name: fake-openai-endpoint-3 - litellm_params: - model: openai/my-fake-model-2 - api_key: my-fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - stream_timeout: 0.001 - rpm: 1000 - - model_name: bad-model - litellm_params: - model: openai/bad-model - api_key: os.environ/OPENAI_API_KEY - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - mock_timeout: True - timeout: 60 - rpm: 1000 - model_info: - health_check_timeout: 1 - - model_name: good-model - litellm_params: - model: openai/bad-model - api_key: os.environ/OPENAI_API_KEY - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - rpm: 1000 - model_info: - health_check_timeout: 1 - - model_name: "*" - litellm_params: - model: openai/* - api_key: os.environ/OPENAI_API_KEY - - model_name: realtime-v1 - litellm_params: - model: azure/gpt-realtime-20250828-standard - api_version: "2025-08-28" - realtime_protocol: GA # Possible values: "GA"/ "v1", "beta" - - - model_name: realtime-beta - litellm_params: - model: azure/gpt-realtime-20250828-standard - api_version: 2025-04-01-preview - - - # provider specific wildcard routing - - model_name: "anthropic/*" - litellm_params: - model: "anthropic/*" + model: anthropic/claude-sonnet-4-6 api_key: os.environ/ANTHROPIC_API_KEY - - model_name: "bedrock/*" + - model_name: gemini-2.5-flash-lite litellm_params: - model: "bedrock/*" - - model_name: "groq/*" + model: gemini/gemini-2.5-flash-lite + - model_name: azure-fake-gpt-5-batch-2025-08-07 litellm_params: - model: "groq/*" - api_key: os.environ/GROQ_API_KEY - - model_name: mistral-embed - litellm_params: - model: mistral/mistral-embed - - model_name: gpt-instruct # [PROD TEST] - tests if `/health` automatically infers this to be a text completion model - litellm_params: - model: text-completion-openai/gpt-3.5-turbo-instruct - - model_name: fake-openai-endpoint-5 - litellm_params: - model: openai/my-fake-model - api_key: my-fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - timeout: 1 - - model_name: badly-configured-openai-endpoint - litellm_params: - model: openai/my-fake-model - api_key: my-fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.appxxxx/ - - model_name: gemini-1.5-flash - litellm_params: - model: gemini/gemini-1.5-flash - api_key: os.environ/GOOGLE_API_KEY - - model_name: gpt-4o - litellm_params: - model: gpt-4o - api_key: os.environ/OPENAI_API_KEY + model: azure/gpt-5 + api_key: asasas + api_base: http://0.0.0.0:8090 +# litellm_settings: +# success_callback: ["s3_v2"] +# s3_callback_params: +# s3_bucket_name: logs-bucket-litellm +# s3_region_name: us-west-2 +# s3_aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID +# s3_aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY +# s3_endpoint_url: http://0.0.0.0:8090 # Your custom endpoint URL -litellm_settings: - # set_verbose: True # Uncomment this if you want to see verbose logs; not recommended in production - drop_params: True - success_callback: ["prometheus"] - # max_budget: 100 - # budget_duration: 30d - num_retries: 5 - request_timeout: 600 - telemetry: False - context_window_fallbacks: [{"gpt-3.5-turbo": ["gpt-3.5-turbo-large"]}] - default_team_settings: - - team_id: team-1 - success_callback: ["langfuse"] - failure_callback: ["langfuse"] - langfuse_public_key: os.environ/LANGFUSE_PROJECT1_PUBLIC # Project 1 - langfuse_secret: os.environ/LANGFUSE_PROJECT1_SECRET # Project 1 - - team_id: team-2 - success_callback: ["langfuse"] - failure_callback: ["langfuse"] - langfuse_public_key: os.environ/LANGFUSE_PROJECT2_PUBLIC # Project 2 - langfuse_secret: os.environ/LANGFUSE_PROJECT2_SECRET # Project 2 - langfuse_host: https://us.cloud.langfuse.com - # cache: true # [OPTIONAL] use for caching responses - # enable_caching_on_provider_specific_optional_params: True # Include provider-specific params in cache keys - # cache_params: # And for shared health check - # type: redis - # host: localhost - # port: 6379 - -# For /fine_tuning/jobs endpoints -finetune_settings: - - custom_llm_provider: azure - api_base: os.environ/AZURE_API_BASE - api_key: os.environ/AZURE_API_KEY - api_version: "2023-03-15-preview" - - custom_llm_provider: openai - api_key: os.environ/OPENAI_API_KEY - -# for /files endpoints -files_settings: - - custom_llm_provider: azure - api_base: os.environ/AZURE_API_BASE - api_key: os.environ/AZURE_API_KEY - api_version: "2023-03-15-preview" - - custom_llm_provider: openai - api_key: os.environ/OPENAI_API_KEY - -router_settings: - routing_strategy: usage-based-routing-v2 - redis_host: os.environ/REDIS_HOST - redis_password: os.environ/REDIS_PASSWORD - redis_port: os.environ/REDIS_PORT - enable_pre_call_checks: true - model_group_alias: {"my-special-fake-model-alias-name": "fake-openai-endpoint-3"} - -general_settings: - master_key: sk-1234 # [OPTIONAL] Use to enforce auth on proxy. See - https://docs.litellm.ai/docs/proxy/virtual_keys - store_model_in_db: True - proxy_budget_rescheduler_min_time: 60 - proxy_budget_rescheduler_max_time: 64 - proxy_batch_write_at: 1 - database_connection_pool_limit: 10 - # background_health_checks: true - # use_shared_health_check: true - # health_check_interval: 30 - # database_url: "postgresql://:@:/" # [OPTIONAL] use for token-based auth to proxy - - pass_through_endpoints: - - path: "/v1/rerank" # route you want to add to LiteLLM Proxy Server - target: "https://api.cohere.com/v1/rerank" # URL this route should forward requests to - headers: # headers to forward to this URL - content-type: application/json # (Optional) Extra Headers to pass to this endpoint - accept: application/json - forward_headers: True - -# environment_variables: - # settings for using redis caching - # REDIS_HOST: redis-16337.c322.us-east-1-2.ec2.cloud.redislabs.com - # REDIS_PORT: "16337" - # REDIS_PASSWORD: \ No newline at end of file +# general_settings: +# proxy_batch_polling_interval: 1000 \ No newline at end of file diff --git a/tests/search_tests/test_duckduckgo_search.py b/tests/search_tests/test_duckduckgo_search.py index a0e5e8ea8ba..13df1cff9d4 100644 --- a/tests/search_tests/test_duckduckgo_search.py +++ b/tests/search_tests/test_duckduckgo_search.py @@ -24,7 +24,103 @@ class TestDuckDuckGoSearch(BaseSearchTest): Return search_provider for DuckDuckGo Search. """ return "duckduckgo" + + @pytest.mark.asyncio + async def test_basic_search(self): + """ + Test basic search functionality with a simple query. + """ + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm._turn_on_debug() + search_provider = self.get_search_provider() + print("Search Provider=", search_provider) + try: + response = await litellm.asearch( + query="india", + search_provider=search_provider, + ) + print("Search response=", response.model_dump_json(indent=4)) + + print(f"\n{'='*80}") + print(f"Response type: {type(response)}") + print(f"Response object: {response.object if hasattr(response, 'object') else 'N/A'}") + + # Check if response has expected Search format + assert hasattr(response, "results"), "Response should have 'results' attribute" + assert hasattr(response, "object"), "Response should have 'object' attribute" + assert response.object == "search", f"Expected object='search', got '{response.object}'" + + # Validate results structure + assert isinstance(response.results, list), "results should be a list" + assert len(response.results) > 0, "Should have at least one result" + + # Check first result structure + first_result = response.results[0] + assert hasattr(first_result, "title"), "Result should have 'title' attribute" + assert hasattr(first_result, "url"), "Result should have 'url' attribute" + assert hasattr(first_result, "snippet"), "Result should have 'snippet' attribute" + + print(f"Total results: {len(response.results)}") + print(f"First result title: {first_result.title}") + print(f"First result URL: {first_result.url}") + print(f"First result snippet: {first_result.snippet[:100]}...") + print(f"{'='*80}\n") + + assert len(first_result.title) > 0, "Title should not be empty" + assert len(first_result.url) > 0, "URL should not be empty" + assert len(first_result.snippet) > 0, "Snippet should not be empty" + + # Validate cost tracking in _hidden_params + assert hasattr(response, "_hidden_params"), "Response should have '_hidden_params' attribute" + hidden_params = response._hidden_params + assert "response_cost" in hidden_params, "_hidden_params should contain 'response_cost'" + + response_cost = hidden_params["response_cost"] + assert response_cost is not None, "response_cost should not be None" + assert isinstance(response_cost, (int, float)), "response_cost should be a number" + assert response_cost == 0, "response_cost should be 0" + + print(f"Cost tracking: ${response_cost:.6f}") + + except Exception as e: + pytest.fail(f"Search call failed: {str(e)}") + + + def test_search_response_structure(self): + """ + Test that the Search response has the correct structure. + """ + litellm.set_verbose = True + search_provider = self.get_search_provider() + + response = litellm.search( + query="india", + search_provider=search_provider, + ) + + # Validate response structure + assert hasattr(response, "results"), "Response should have 'results' attribute" + assert hasattr(response, "object"), "Response should have 'object' attribute" + + assert isinstance(response.results, list), "results should be a list" + assert len(response.results) > 0, "Should have at least one result" + assert response.object == "search", "object should be 'search'" + + # Validate first result structure + first_result = response.results[0] + assert hasattr(first_result, "title"), "Result should have 'title' attribute" + assert hasattr(first_result, "url"), "Result should have 'url' attribute" + assert hasattr(first_result, "snippet"), "Result should have 'snippet' attribute" + assert isinstance(first_result.title, str), "title should be a string" + assert isinstance(first_result.url, str), "url should be a string" + assert isinstance(first_result.snippet, str), "snippet should be a string" + + print(f"\nResponse structure validated:") + print(f" - object: {response.object}") + print(f" - results: {len(response.results)}") + print(f" - first result has all required fields") class TestDuckDuckGoSearchMocked: """ @@ -156,10 +252,11 @@ class TestDuckDuckGoSearchMocked: # Get the actual call arguments call_args = mock_get.call_args - # Verify URL contains the query + # Verify URL contains the query with proper URL encoding url = call_args.kwargs["url"] assert "api.duckduckgo.com" in url - assert "q=python" in url or "q=python%20programming" in url + # URL should be properly encoded with %20 for spaces + assert ("q=python+programming" in url or "q=python%20programming" in url) assert "format=json" in url # Verify response structure From 6b8b391116bc7271b1c125ca6ed7765f747ff58d Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 18 Feb 2026 16:17:39 +0530 Subject: [PATCH 3/6] Add duckcukgo in docs --- docs/my-website/docs/search/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/my-website/docs/search/index.md b/docs/my-website/docs/search/index.md index 551a495261a..8a71edead06 100644 --- a/docs/my-website/docs/search/index.md +++ b/docs/my-website/docs/search/index.md @@ -276,6 +276,7 @@ The response follows Perplexity's search format with the following structure: | Firecrawl | `FIRECRAWL_API_KEY` | `firecrawl` | | SearXNG | `SEARXNG_API_BASE` (required) | `searxng` | | Linkup | `LINKUP_API_KEY` | `linkup` | +| DuckDuckGo | `DUCKDUCKGO_API_BASE` | `duckduckgo` | See the individual provider documentation for detailed setup instructions and provider-specific parameters. From 3bc1ae53313372652fae94a2affacd2bf83d7dd2 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 18 Feb 2026 16:21:41 +0530 Subject: [PATCH 4/6] Add duckcukgo in docs --- proxy_server_config.yaml | 242 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 224 insertions(+), 18 deletions(-) diff --git a/proxy_server_config.yaml b/proxy_server_config.yaml index 234f2cd87c5..8ed728c5b28 100644 --- a/proxy_server_config.yaml +++ b/proxy_server_config.yaml @@ -1,25 +1,231 @@ model_list: - - model_name: sonnet-4.6 + - model_name: gpt-3.5-turbo-end-user-test litellm_params: - model: anthropic/claude-sonnet-4-6 + model: gpt-3.5-turbo + region_name: "eu" + model_info: + id: "1" + - model_name: gpt-3.5-turbo-end-user-test + litellm_params: + model: openai/gpt-4.1-mini + api_key: os.environ/OPENAI_API_KEY # The `os.environ/` prefix tells litellm to read this from the env. See https://docs.litellm.ai/docs/simple_proxy#load-api-keys-from-vault + - model_name: gpt-3.5-turbo + litellm_params: + model: openai/gpt-4.1-mini + api_key: os.environ/OPENAI_API_KEY # The `os.environ/` prefix tells litellm to read this from the env. See https://docs.litellm.ai/docs/simple_proxy#load-api-keys-from-vault + - model_name: gpt-3.5-turbo-large + litellm_params: + model: "gpt-3.5-turbo-1106" + api_key: os.environ/OPENAI_API_KEY + rpm: 480 + timeout: 300 + stream_timeout: 60 + - model_name: gpt-4 + litellm_params: + model: openai/gpt-4.1-mini + api_key: os.environ/OPENAI_API_KEY # The `os.environ/` prefix tells litellm to read this from the env. See https://docs.litellm.ai/docs/simple_proxy#load-api-keys-from-vault + rpm: 480 + timeout: 300 + stream_timeout: 60 + - model_name: sagemaker-completion-model + litellm_params: + model: sagemaker/berri-benchmarking-Llama-2-70b-chat-hf-4 + input_cost_per_second: 0.000420 + - model_name: text-embedding-ada-002 + litellm_params: + model: openai/text-embedding-ada-002 + api_key: os.environ/OPENAI_API_KEY + model_info: + mode: embedding + base_model: text-embedding-ada-002 + - model_name: dall-e-2 # some tests use dall-e-2 which is now deprecated, alias to dall-e-3 + litellm_params: + model: openai/dall-e-3 + - model_name: openai-dall-e-3 + litellm_params: + model: dall-e-3 + - model_name: fake-openai-endpoint + litellm_params: + model: openai/gpt-3.5-turbo-0301 + api_key: fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + - model_name: fake-openai-endpoint-2 + litellm_params: + model: openai/my-fake-model + api_key: my-fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + stream_timeout: 0.001 + rpm: 1 + - model_name: fake-openai-endpoint-3 + litellm_params: + model: openai/my-fake-model + api_key: my-fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + stream_timeout: 0.001 + rpm: 1000 + - model_name: fake-openai-endpoint-4 + litellm_params: + model: openai/my-fake-model + api_key: my-fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + num_retries: 50 + - model_name: fake-openai-endpoint-3 + litellm_params: + model: openai/my-fake-model-2 + api_key: my-fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + stream_timeout: 0.001 + rpm: 1000 + - model_name: bad-model + litellm_params: + model: openai/bad-model + api_key: os.environ/OPENAI_API_KEY + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + mock_timeout: True + timeout: 60 + rpm: 1000 + model_info: + health_check_timeout: 1 + - model_name: good-model + litellm_params: + model: openai/bad-model + api_key: os.environ/OPENAI_API_KEY + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + rpm: 1000 + model_info: + health_check_timeout: 1 + - model_name: "*" + litellm_params: + model: openai/* + api_key: os.environ/OPENAI_API_KEY + - model_name: realtime-v1 + litellm_params: + model: azure/gpt-realtime-20250828-standard + api_version: "2025-08-28" + realtime_protocol: GA # Possible values: "GA"/ "v1", "beta" + + - model_name: realtime-beta + litellm_params: + model: azure/gpt-realtime-20250828-standard + api_version: 2025-04-01-preview + + + # provider specific wildcard routing + - model_name: "anthropic/*" + litellm_params: + model: "anthropic/*" api_key: os.environ/ANTHROPIC_API_KEY - - model_name: gemini-2.5-flash-lite + - model_name: "bedrock/*" litellm_params: - model: gemini/gemini-2.5-flash-lite - - model_name: azure-fake-gpt-5-batch-2025-08-07 + model: "bedrock/*" + - model_name: "groq/*" litellm_params: - model: azure/gpt-5 - api_key: asasas - api_base: http://0.0.0.0:8090 + model: "groq/*" + api_key: os.environ/GROQ_API_KEY + - model_name: mistral-embed + litellm_params: + model: mistral/mistral-embed + - model_name: gpt-instruct # [PROD TEST] - tests if `/health` automatically infers this to be a text completion model + litellm_params: + model: text-completion-openai/gpt-3.5-turbo-instruct + - model_name: fake-openai-endpoint-5 + litellm_params: + model: openai/my-fake-model + api_key: my-fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + timeout: 1 + - model_name: badly-configured-openai-endpoint + litellm_params: + model: openai/my-fake-model + api_key: my-fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.appxxxx/ + - model_name: gemini-1.5-flash + litellm_params: + model: gemini/gemini-1.5-flash + api_key: os.environ/GOOGLE_API_KEY + - model_name: gpt-4o + litellm_params: + model: gpt-4o + api_key: os.environ/OPENAI_API_KEY -# litellm_settings: -# success_callback: ["s3_v2"] -# s3_callback_params: -# s3_bucket_name: logs-bucket-litellm -# s3_region_name: us-west-2 -# s3_aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID -# s3_aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY -# s3_endpoint_url: http://0.0.0.0:8090 # Your custom endpoint URL -# general_settings: -# proxy_batch_polling_interval: 1000 \ No newline at end of file +litellm_settings: + # set_verbose: True # Uncomment this if you want to see verbose logs; not recommended in production + drop_params: True + success_callback: ["prometheus"] + # max_budget: 100 + # budget_duration: 30d + num_retries: 5 + request_timeout: 600 + telemetry: False + context_window_fallbacks: [{"gpt-3.5-turbo": ["gpt-3.5-turbo-large"]}] + default_team_settings: + - team_id: team-1 + success_callback: ["langfuse"] + failure_callback: ["langfuse"] + langfuse_public_key: os.environ/LANGFUSE_PROJECT1_PUBLIC # Project 1 + langfuse_secret: os.environ/LANGFUSE_PROJECT1_SECRET # Project 1 + - team_id: team-2 + success_callback: ["langfuse"] + failure_callback: ["langfuse"] + langfuse_public_key: os.environ/LANGFUSE_PROJECT2_PUBLIC # Project 2 + langfuse_secret: os.environ/LANGFUSE_PROJECT2_SECRET # Project 2 + langfuse_host: https://us.cloud.langfuse.com + # cache: true # [OPTIONAL] use for caching responses + # enable_caching_on_provider_specific_optional_params: True # Include provider-specific params in cache keys + # cache_params: # And for shared health check + # type: redis + # host: localhost + # port: 6379 + +# For /fine_tuning/jobs endpoints +finetune_settings: + - custom_llm_provider: azure + api_base: os.environ/AZURE_API_BASE + api_key: os.environ/AZURE_API_KEY + api_version: "2023-03-15-preview" + - custom_llm_provider: openai + api_key: os.environ/OPENAI_API_KEY + +# for /files endpoints +files_settings: + - custom_llm_provider: azure + api_base: os.environ/AZURE_API_BASE + api_key: os.environ/AZURE_API_KEY + api_version: "2023-03-15-preview" + - custom_llm_provider: openai + api_key: os.environ/OPENAI_API_KEY + +router_settings: + routing_strategy: usage-based-routing-v2 + redis_host: os.environ/REDIS_HOST + redis_password: os.environ/REDIS_PASSWORD + redis_port: os.environ/REDIS_PORT + enable_pre_call_checks: true + model_group_alias: {"my-special-fake-model-alias-name": "fake-openai-endpoint-3"} + +general_settings: + master_key: sk-1234 # [OPTIONAL] Use to enforce auth on proxy. See - https://docs.litellm.ai/docs/proxy/virtual_keys + store_model_in_db: True + proxy_budget_rescheduler_min_time: 60 + proxy_budget_rescheduler_max_time: 64 + proxy_batch_write_at: 1 + database_connection_pool_limit: 10 + # background_health_checks: true + # use_shared_health_check: true + # health_check_interval: 30 + # database_url: "postgresql://:@:/" # [OPTIONAL] use for token-based auth to proxy + + pass_through_endpoints: + - path: "/v1/rerank" # route you want to add to LiteLLM Proxy Server + target: "https://api.cohere.com/v1/rerank" # URL this route should forward requests to + headers: # headers to forward to this URL + content-type: application/json # (Optional) Extra Headers to pass to this endpoint + accept: application/json + forward_headers: True + +# environment_variables: + # settings for using redis caching + # REDIS_HOST: redis-16337.c322.us-east-1-2.ec2.cloud.redislabs.com + # REDIS_PORT: "16337" + # REDIS_PASSWORD: \ No newline at end of file From b8fd5698f8e8511a4758e78c1bb6ffb4d66bca35 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 18 Feb 2026 18:23:54 +0530 Subject: [PATCH 5/6] Add docs for DuckDuckGo --- provider_endpoints_support.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 366da0c0b46..328398a296a 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -761,6 +761,23 @@ "interactions": true } }, + "duckduckgo": { + "display_name": "DuckDuckGo (`duckduckgo`)", + "url": "https://docs.litellm.ai/docs/search/duckduckgo", + "endpoints": { + "chat_completions": false, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "search": true + } + }, "elevenlabs": { "display_name": "ElevenLabs (`elevenlabs`)", "url": "https://docs.litellm.ai/docs/providers/elevenlabs", From 5f70165a98239af2fae3c7b292ca32b80eef8165 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 18 Feb 2026 18:32:25 +0530 Subject: [PATCH 6/6] Fix get_unique_names_from_llms_dir --- tests/code_coverage_tests/enforce_llms_folder_style.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/code_coverage_tests/enforce_llms_folder_style.py b/tests/code_coverage_tests/enforce_llms_folder_style.py index f684d884a6b..7e6fd8e6fd6 100644 --- a/tests/code_coverage_tests/enforce_llms_folder_style.py +++ b/tests/code_coverage_tests/enforce_llms_folder_style.py @@ -16,6 +16,7 @@ SEARCH_PROVIDERS = [ "firecrawl", "searxng", "linkup", + "duckduckgo", ] ALLOWED_FILES_IN_LLMS_FOLDER = [ @@ -73,8 +74,8 @@ def run_lint_check(unique_names): def main(): - llms_dir = "./litellm/llms/" # Update this path if needed - # llms_dir = "../../litellm/llms/" # LOCAL TESTING + # llms_dir = "./litellm/llms/" # Update this path if needed + llms_dir = "litellm/litellm/llms" # LOCAL TESTING unique_names = get_unique_names_from_llms_dir(llms_dir) print("Unique names in llms directory:", sorted(list(unique_names)))