diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index 2c05ee827a3..f20cf976d03 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -301,6 +301,7 @@ jobs:
tests/unit/llms/inception
tests/unit/llms/infinity
tests/unit/llms/jina_ai
+ tests/unit/llms/keenable
tests/unit/llms/lambda_ai
tests/unit/llms/langflow
tests/unit/llms/langgraph
diff --git a/litellm/llms/keenable/search/__init__.py b/litellm/llms/keenable/search/__init__.py
new file mode 100644
index 00000000000..322ee824809
--- /dev/null
+++ b/litellm/llms/keenable/search/__init__.py
@@ -0,0 +1,7 @@
+"""
+Keenable Search API module.
+"""
+
+from litellm.llms.keenable.search.transformation import KeenableSearchConfig
+
+__all__ = ["KeenableSearchConfig"]
diff --git a/litellm/llms/keenable/search/transformation.py b/litellm/llms/keenable/search/transformation.py
new file mode 100644
index 00000000000..9ac4cb32e62
--- /dev/null
+++ b/litellm/llms/keenable/search/transformation.py
@@ -0,0 +1,169 @@
+"""
+Calls Keenable's /v1/search endpoint to search the web.
+
+Keenable API Reference: https://docs.keenable.ai/api-reference/search
+"""
+
+from collections.abc import Iterable, Mapping
+from typing import Final, TypedDict
+
+import httpx
+from pydantic import ConfigDict, TypeAdapter
+from typing_extensions import NotRequired, ReadOnly
+
+from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
+from litellm.litellm_core_utils.url_utils import ensure_api_route
+from litellm.llms.base_llm.search.transformation import (
+ BaseSearchConfig,
+ SearchResponse,
+ SearchResult,
+)
+from litellm.secret_managers.main import get_secret_str
+
+_JSON_OBJECT: Final = TypeAdapter(Mapping[str, object], config=ConfigDict(hide_input_in_errors=True))
+_JSON_VALUES: Final = TypeAdapter(Iterable[object], config=ConfigDict(hide_input_in_errors=True))
+_UNIFIED_PARAMS: Final = frozenset({"max_results", "search_domain_filter", "country", "max_tokens_per_page"})
+
+
+class KeenableSearchRequest(TypedDict):
+ """
+ Keenable Search API request format.
+ Based on: https://docs.keenable.ai/api-reference/search
+ """
+
+ query: ReadOnly[str]
+ mode: NotRequired[ReadOnly[str]]
+ site: NotRequired[ReadOnly[str]]
+ acquired_after: NotRequired[ReadOnly[str]]
+ acquired_before: NotRequired[ReadOnly[str]]
+ published_after: NotRequired[ReadOnly[str]]
+ published_before: NotRequired[ReadOnly[str]]
+ query_time: NotRequired[ReadOnly[str]]
+ snippet_max_length: NotRequired[ReadOnly[int]]
+ max_results: NotRequired[ReadOnly[int]]
+
+
+def _first_domain(domains: object) -> str | None:
+ if isinstance(domains, list) and domains and isinstance(domains[0], str) and domains[0]:
+ return domains[0]
+ if isinstance(domains, str) and domains:
+ return domains
+ return None
+
+
+def _search_result(raw_result: object) -> SearchResult:
+ result: Final = _JSON_OBJECT.validate_python(raw_result)
+ return SearchResult.model_validate(
+ {
+ "title": result.get("title", ""),
+ "url": result.get("url", ""),
+ "snippet": result.get("snippet") or result.get("description") or "",
+ "date": result.get("published_at"),
+ "last_updated": result.get("acquired_at"),
+ }
+ )
+
+
+class KeenableSearchConfig(BaseSearchConfig):
+ KEENABLE_API_BASE = "https://api.keenable.ai/v1"
+
+ @staticmethod
+ def ui_friendly_name() -> str:
+ return "Keenable"
+
+ def validate_environment(
+ self,
+ headers: dict,
+ api_key: str | None = None,
+ api_base: str | None = None,
+ **kwargs,
+ ) -> dict:
+ """
+ Authenticated calls send ``X-API-Key``. Without a key, Keenable's public
+ search tier still works and requires ``X-Keenable-Title`` instead.
+ """
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("KEENABLE_API_KEY",),
+ base_env_var="KEENABLE_API_BASE",
+ default_api_base=self.KEENABLE_API_BASE,
+ )
+ headers["Content-Type"] = "application/json"
+ if api_key:
+ headers["X-API-Key"] = api_key
+ else:
+ headers["X-Keenable-Title"] = get_secret_str("KEENABLE_TITLE") or "LiteLLM"
+ return headers
+
+ def get_complete_url(
+ self,
+ api_base: str | None,
+ optional_params: dict,
+ data: dict | list[dict] | None = None,
+ **kwargs,
+ ) -> str:
+ api_key: Final = kwargs.get("api_key") or get_secret_str("KEENABLE_API_KEY")
+ route: Final = "search" if api_key else "search/public"
+ return ensure_api_route(api_base or get_secret_str("KEENABLE_API_BASE") or self.KEENABLE_API_BASE, route)
+
+ def transform_search_request(
+ self,
+ query: str | list[str],
+ optional_params: dict,
+ **kwargs,
+ ) -> dict:
+ """
+ Transform Search request to Keenable API format.
+
+ Transforms Perplexity unified spec parameters:
+ - query → query (same)
+ - max_results → max_results (same)
+ - search_domain_filter → site (first domain only; Keenable takes a single site)
+ - country → (not applicable, ignored)
+ - max_tokens_per_page → (not applicable, ignored; Keenable's snippet_max_length is
+ characters in 180–10000, not tokens)
+
+ All other Keenable-specific parameters are passed through as-is.
+ """
+ site: Final = _first_domain(optional_params.get("search_domain_filter"))
+ unified: Final[KeenableSearchRequest] = {
+ "query": " ".join(query) if isinstance(query, list) else query,
+ **({"max_results": optional_params["max_results"]} if "max_results" in optional_params else {}),
+ **({"site": site} if site is not None else {}),
+ }
+ passthrough: Final = {
+ param: value
+ for param, value in optional_params.items()
+ if param not in _UNIFIED_PARAMS and param not in unified
+ }
+ return {**unified, **passthrough}
+
+ def transform_search_response(
+ self,
+ raw_response: httpx.Response,
+ logging_obj: LiteLLMLoggingObj,
+ **kwargs,
+ ) -> SearchResponse:
+ """
+ Transform Keenable API response to LiteLLM unified SearchResponse format.
+
+ Keenable → LiteLLM mappings:
+ - results[].title → SearchResult.title
+ - results[].url → SearchResult.url
+ - results[].snippet → SearchResult.snippet
+ - results[].published_at → SearchResult.date
+ - results[].acquired_at → SearchResult.last_updated
+ - every top-level key except results → SearchResponse.provider_metadata
+ """
+ response_json: Final = _JSON_OBJECT.validate_python(raw_response.json())
+ return SearchResponse.model_validate(
+ {
+ "results": tuple(
+ _search_result(raw_result)
+ for raw_result in _JSON_VALUES.validate_python(response_json.get("results", []))
+ ),
+ "object": "search",
+ "provider_metadata": {key: value for key, value in response_json.items() if key != "results"},
+ }
+ )
diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json
index d68eb3ce775..715d880b8df 100644
--- a/litellm/model_prices_and_context_window_backup.json
+++ b/litellm/model_prices_and_context_window_backup.json
@@ -24657,6 +24657,16 @@
}
]
},
+ "keenable/search": {
+ "input_cost_per_query": 0.004,
+ "litellm_provider": "keenable",
+ "mode": "search",
+ "metadata": {
+ "notes": "Pay-as-you-go is $4 per 1,000 requests after 100K free/month (https://keenable.ai/pricing, retrieved 2026-11-02).",
+ "source": "https://keenable.ai/pricing",
+ "source_date": "2026-11-02"
+ }
+ },
"firecrawl/search": {
"litellm_provider": "firecrawl",
"mode": "search",
diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json
index 700a35edb06..7302e50d11f 100644
--- a/litellm/provider_endpoints_support_backup.json
+++ b/litellm/provider_endpoints_support_backup.json
@@ -1556,6 +1556,32 @@
"generateContent": false
}
},
+ "keenable": {
+ "display_name": "Keenable (`keenable`)",
+ "url": "https://docs.keenable.ai/api-reference/search",
+ "endpoints": {
+ "chat_completions": false,
+ "responses": false,
+ "text_completion": false,
+ "embeddings": false,
+ "image_generations": false,
+ "image_edits": false,
+ "audio_transcriptions": false,
+ "audio_speech": false,
+ "rerank": false,
+ "ocr": false,
+ "search": true,
+ "moderations": false,
+ "realtime": false,
+ "video_generations": false,
+ "batches": false,
+ "messages": false,
+ "interactions": false,
+ "systemone": false,
+ "a2a": false,
+ "generateContent": false
+ }
+ },
"brave": {
"display_name": "Brave Search (`brave`)",
"url": "https://docs.litellm.ai/docs/search/brave",
diff --git a/litellm/proxy/mcp_registry.json b/litellm/proxy/mcp_registry.json
index 70e81f127c9..c767ae2929e 100644
--- a/litellm/proxy/mcp_registry.json
+++ b/litellm/proxy/mcp_registry.json
@@ -269,6 +269,19 @@
{"name": "EXA_API_KEY", "description": "Exa API Key", "secret": true}
]
},
+ {
+ "name": "keenable",
+ "title": "Keenable",
+ "description": "Web search and page fetch for AI agents",
+ "icon_url": "/ui/assets/logos/keenable.png",
+ "category": "Search",
+ "registry_url": null,
+ "transport": "http",
+ "url": "https://api.keenable.ai/mcp",
+ "env_vars": [
+ {"name": "KEENABLE_API_KEY", "description": "Keenable API Key", "secret": true}
+ ]
+ },
{
"name": "tavily",
"title": "Tavily",
diff --git a/litellm/types/utils.py b/litellm/types/utils.py
index da1d99957a7..48f0ab94baf 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -4736,6 +4736,7 @@ class SearchProviders(str, Enum):
TAVILY = "tavily"
PARALLEL_AI = "parallel_ai"
EXA_AI = "exa_ai"
+ KEENABLE = "keenable"
BRAVE = "brave"
GOOGLE_PSE = "google_pse"
DATAFORSEO = "dataforseo"
diff --git a/litellm/utils.py b/litellm/utils.py
index b0826f84006..8dd7428a271 100644
--- a/litellm/utils.py
+++ b/litellm/utils.py
@@ -11128,6 +11128,7 @@ class ProviderConfigManager:
from litellm.llms.fastcrw.search.transformation import FastCRWSearchConfig
from litellm.llms.firecrawl.search.transformation import FirecrawlSearchConfig
from litellm.llms.google_pse.search.transformation import GooglePSESearchConfig
+ from litellm.llms.keenable.search.transformation import KeenableSearchConfig
from litellm.llms.linkup.search.transformation import LinkupSearchConfig
from litellm.llms.nimble.search.transformation import NimbleSearchConfig
from litellm.llms.parallel_ai.search.transformation import (
@@ -11147,6 +11148,7 @@ class ProviderConfigManager:
SearchProviders.TAVILY: TavilySearchConfig,
SearchProviders.PARALLEL_AI: ParallelAISearchConfig,
SearchProviders.EXA_AI: ExaAISearchConfig,
+ SearchProviders.KEENABLE: KeenableSearchConfig,
SearchProviders.BRAVE: BraveSearchConfig,
SearchProviders.GOOGLE_PSE: GooglePSESearchConfig,
SearchProviders.DATAFORSEO: DataForSEOSearchConfig,
diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json
index d68eb3ce775..715d880b8df 100644
--- a/model_prices_and_context_window.json
+++ b/model_prices_and_context_window.json
@@ -24657,6 +24657,16 @@
}
]
},
+ "keenable/search": {
+ "input_cost_per_query": 0.004,
+ "litellm_provider": "keenable",
+ "mode": "search",
+ "metadata": {
+ "notes": "Pay-as-you-go is $4 per 1,000 requests after 100K free/month (https://keenable.ai/pricing, retrieved 2026-11-02).",
+ "source": "https://keenable.ai/pricing",
+ "source_date": "2026-11-02"
+ }
+ },
"firecrawl/search": {
"litellm_provider": "firecrawl",
"mode": "search",
diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json
index 700a35edb06..2889b0d3f17 100644
--- a/provider_endpoints_support.json
+++ b/provider_endpoints_support.json
@@ -1556,6 +1556,32 @@
"generateContent": false
}
},
+ "keenable": {
+ "display_name": "Keenable (`keenable`)",
+ "url": "https://docs.keenable.ai/api-reference/search",
+ "endpoints": {
+ "chat_completions": false,
+ "responses": false,
+ "text_completion": false,
+ "embeddings": false,
+ "image_generations": false,
+ "image_edits": false,
+ "audio_transcriptions": false,
+ "audio_speech": false,
+ "rerank": false,
+ "ocr": false,
+ "search": true,
+ "moderations": false,
+ "realtime": false,
+ "video_generations": false,
+ "batches": false,
+ "messages": false,
+ "interactions": false,
+ "systemone": false,
+ "a2a": false,
+ "generateContent": false
+ }
+ },
"brave": {
"display_name": "Brave Search (`brave`)",
"url": "https://docs.litellm.ai/docs/search/brave",
diff --git a/tests/code_coverage_tests/enforce_llms_folder_style.py b/tests/code_coverage_tests/enforce_llms_folder_style.py
index a284cf9e1a9..9d8f5542027 100644
--- a/tests/code_coverage_tests/enforce_llms_folder_style.py
+++ b/tests/code_coverage_tests/enforce_llms_folder_style.py
@@ -10,6 +10,7 @@ SEARCH_PROVIDERS = [
"google_pse",
"parallel_ai",
"exa_ai",
+ "keenable",
"brave",
"firecrawl",
"fastcrw",
diff --git a/tests/unit/llms/base_llm/search/test_base_search_transformation.py b/tests/unit/llms/base_llm/search/test_base_search_transformation.py
index e6aad7688d1..b68d58c8cde 100644
--- a/tests/unit/llms/base_llm/search/test_base_search_transformation.py
+++ b/tests/unit/llms/base_llm/search/test_base_search_transformation.py
@@ -27,6 +27,7 @@ from litellm.llms.exa_ai.search.transformation import ExaAISearchConfig
from litellm.llms.fastcrw.search.transformation import FastCRWSearchConfig
from litellm.llms.firecrawl.search.transformation import FirecrawlSearchConfig
from litellm.llms.google_pse.search.transformation import GooglePSESearchConfig
+from litellm.llms.keenable.search.transformation import KeenableSearchConfig
from litellm.llms.linkup.search.transformation import LinkupSearchConfig
from litellm.llms.nimble.search.transformation import NimbleSearchConfig
from litellm.llms.parallel_ai.search.transformation import ParallelAISearchConfig
@@ -79,6 +80,7 @@ PROVIDERS: Tuple[ProviderSpec, ...] = (
(PerplexitySearchConfig, {"PERPLEXITYAI_API_KEY": "srv"}, "caller-key", {}),
(APISerpentSearchConfig, {"APISERPENT_API_KEY": "srv"}, "caller-key", {}),
(ExaAISearchConfig, {"EXA_API_KEY": "srv"}, "caller-key", {}),
+ (KeenableSearchConfig, {"KEENABLE_API_KEY": "srv"}, "caller-key", {}),
(BraveSearchConfig, {"BRAVE_API_KEY": "srv"}, "caller-key", {}),
(FirecrawlSearchConfig, {"FIRECRAWL_API_KEY": "srv"}, "caller-key", {}),
(LinkupSearchConfig, {"LINKUP_API_KEY": "srv"}, "caller-key", {}),
diff --git a/tests/unit/llms/keenable/search/test_transformation.py b/tests/unit/llms/keenable/search/test_transformation.py
new file mode 100644
index 00000000000..de6e2d76fdb
--- /dev/null
+++ b/tests/unit/llms/keenable/search/test_transformation.py
@@ -0,0 +1,179 @@
+import json
+from typing import Final
+from unittest.mock import Mock
+
+import httpx
+import pytest
+import respx
+
+import litellm
+from litellm.llms.keenable.search.transformation import KeenableSearchConfig
+
+KEENABLE_SEARCH_URL: Final = "https://api.keenable.ai/v1/search"
+
+
+@pytest.fixture
+def keenable_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("KEENABLE_API_KEY", "test-keenable-key")
+ monkeypatch.delenv("KEENABLE_API_BASE", raising=False)
+
+
+@pytest.mark.usefixtures("keenable_api_key")
+def test_search_maps_keenable_results_to_search_response(respx_mock: respx.MockRouter) -> None:
+ respx_mock.post(KEENABLE_SEARCH_URL).respond(
+ json={
+ "query": "typescript best practices",
+ "mode": "pro",
+ "results": [
+ {
+ "title": "TypeScript Best Practices 2026",
+ "url": "https://example.com/ts-best-practices",
+ "description": "",
+ "snippet": "Use strict mode and prefer interfaces.",
+ "published_at": "2026-01-15T10:30:00Z",
+ "acquired_at": "2026-01-16T08:12:34Z",
+ },
+ {
+ "title": "Second",
+ "url": "https://example.com/second",
+ "snippet": "Second snippet.",
+ },
+ ],
+ }
+ )
+
+ response: Final = litellm.search(query="typescript best practices", search_provider="keenable")
+
+ assert response.object == "search"
+ assert len(response.results) == 2
+ first: Final = response.results[0]
+ assert first.title == "TypeScript Best Practices 2026"
+ assert first.url == "https://example.com/ts-best-practices"
+ assert first.snippet == "Use strict mode and prefer interfaces."
+ assert first.date == "2026-01-15T10:30:00Z"
+ assert first.last_updated == "2026-01-16T08:12:34Z"
+ assert response.results[1].url == "https://example.com/second"
+ assert response.model_dump(mode="json")["provider_metadata"] == {
+ "query": "typescript best practices",
+ "mode": "pro",
+ }
+
+
+@pytest.mark.usefixtures("keenable_api_key")
+def test_search_sends_unified_params_and_api_key(respx_mock: respx.MockRouter) -> None:
+ route: Final = respx_mock.post(KEENABLE_SEARCH_URL).respond(
+ json={"results": [{"title": "ML", "url": "https://example.com/ml", "snippet": "Machine learning."}]}
+ )
+
+ response: Final = litellm.search(
+ query="machine learning",
+ search_provider="keenable",
+ max_results=5,
+ search_domain_filter=["arxiv.org", "nature.com"],
+ max_tokens_per_page=50,
+ )
+
+ assert json.loads(route.calls.last.request.content) == {
+ "query": "machine learning",
+ "max_results": 5,
+ "site": "arxiv.org",
+ }
+ assert route.calls.last.request.headers["x-api-key"] == "test-keenable-key"
+ assert [result.url for result in response.results] == ["https://example.com/ml"]
+
+
+@pytest.mark.usefixtures("keenable_api_key")
+def test_search_ignores_max_tokens_per_page(respx_mock: respx.MockRouter) -> None:
+ route: Final = respx_mock.post(KEENABLE_SEARCH_URL).respond(json={"results": []})
+
+ litellm.search(query="q", search_provider="keenable", max_tokens_per_page=50)
+
+ assert "snippet_max_length" not in json.loads(route.calls.last.request.content)
+
+
+@pytest.mark.usefixtures("keenable_api_key")
+def test_search_joins_list_query_and_passes_keenable_params(respx_mock: respx.MockRouter) -> None:
+ route: Final = respx_mock.post(KEENABLE_SEARCH_URL).respond(json={"results": []})
+
+ litellm.search(
+ query=["AI news", "ML trends"],
+ search_provider="keenable",
+ mode="realtime",
+ published_after="7d",
+ )
+
+ assert json.loads(route.calls.last.request.content) == {
+ "query": "AI news ML trends",
+ "mode": "realtime",
+ "published_after": "7d",
+ }
+
+
+def test_transform_search_response_falls_back_to_description_when_snippet_missing() -> None:
+ response: Final = KeenableSearchConfig().transform_search_response(
+ httpx.Response(
+ 200,
+ json={
+ "results": [
+ {
+ "title": "Legacy",
+ "url": "https://example.com/legacy",
+ "description": "old description field",
+ }
+ ]
+ },
+ ),
+ logging_obj=Mock(),
+ )
+
+ assert response.results[0].snippet == "old description field"
+
+
+def test_validate_environment_uses_title_header_when_keyless(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KEENABLE_API_KEY", raising=False)
+ monkeypatch.delenv("KEENABLE_TITLE", raising=False)
+
+ headers: Final = KeenableSearchConfig().validate_environment(headers={})
+
+ assert headers["X-Keenable-Title"] == "LiteLLM"
+ assert "X-API-Key" not in headers
+
+
+@pytest.mark.usefixtures("keenable_api_key")
+@pytest.mark.parametrize(
+ "api_base",
+ [None, "https://api.keenable.ai/v1", "https://api.keenable.ai/v1/"],
+)
+def test_get_complete_url_appends_search_once(api_base: str | None, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KEENABLE_API_BASE", raising=False)
+
+ assert KeenableSearchConfig().get_complete_url(
+ api_base=api_base, optional_params={}, api_key="test-keenable-key"
+ ) == ("https://api.keenable.ai/v1/search")
+
+
+def test_get_complete_url_uses_public_route_when_keyless(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KEENABLE_API_KEY", raising=False)
+ monkeypatch.delenv("KEENABLE_API_BASE", raising=False)
+
+ assert KeenableSearchConfig().get_complete_url(api_base=None, optional_params={}) == (
+ "https://api.keenable.ai/v1/search/public"
+ )
+
+
+def test_search_keyless_hits_public_route_with_title_header(
+ monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter
+) -> None:
+ monkeypatch.delenv("KEENABLE_API_KEY", raising=False)
+ monkeypatch.delenv("KEENABLE_API_BASE", raising=False)
+ monkeypatch.delenv("KEENABLE_TITLE", raising=False)
+ route: Final = respx_mock.post("https://api.keenable.ai/v1/search/public").respond(
+ json={"results": [{"title": "T", "url": "https://example.com", "snippet": "S"}]}
+ )
+
+ response: Final = litellm.search(query="q", search_provider="keenable")
+
+ assert route.calls.last.request.headers["x-keenable-title"] == "LiteLLM"
+ assert "x-api-key" not in {k.lower(): v for k, v in route.calls.last.request.headers.items()}
+ assert [result.url for result in response.results] == ["https://example.com"]
+
diff --git a/ui/litellm-dashboard/public/assets/logos/keenable.png b/ui/litellm-dashboard/public/assets/logos/keenable.png
new file mode 100644
index 00000000000..5865601dd3d
Binary files /dev/null and b/ui/litellm-dashboard/public/assets/logos/keenable.png differ
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.test.tsx
index f860b1a8bd4..c2c930c7549 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.test.tsx
@@ -15,6 +15,12 @@ describe("SearchProviderLabel", () => {
expect(img).toHaveAttribute("src", expect.stringContaining("exa_ai.png"));
});
+ it("renders the keenable logo file for the keenable slug", () => {
+ render();
+ const img = screen.getByRole("img", { name: "Keenable logo" });
+ expect(img).toHaveAttribute("src", expect.stringContaining("keenable.png"));
+ });
+
it("renders the google_pse logo file for the google_pse slug", () => {
render();
expect(screen.getByRole("img", { name: "Google PSE logo" })).toHaveAttribute(
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx
index b42484dbe71..ab273589414 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx
@@ -31,6 +31,7 @@ import bingLogo from "../../../../../public/assets/logos/bing.png";
import dataforseoLogo from "../../../../../public/assets/logos/dataforseo.png";
import exaAiLogo from "../../../../../public/assets/logos/exa_ai.png";
import googlePseLogo from "../../../../../public/assets/logos/google_pse.png";
+import keenableLogo from "../../../../../public/assets/logos/keenable.png";
import nimbleLogo from "../../../../../public/assets/logos/nimble.png";
import parallelAiLogo from "../../../../../public/assets/logos/parallel_ai.png";
import perplexityLogo from "../../../../../public/assets/logos/perplexity.png";
@@ -42,6 +43,7 @@ const searchProviderLogoMap: Record = {
tavily: tavilyLogo.src,
parallel_ai: parallelAiLogo.src,
exa_ai: exaAiLogo.src,
+ keenable: keenableLogo.src,
google_pse: googlePseLogo.src,
dataforseo: dataforseoLogo.src,
nimble: nimbleLogo.src,