feat(web): allow Keenable search without an API key

Keenable's public endpoint serves the free tier (1000 req/hour) without
auth. Route to /v1/search/public with the X-Keenable-Title header when no
key is configured, instead of falling back to DuckDuckGo; keep the
authenticated /v1/search path when an apiKey or KEENABLE_API_KEY is set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilya Gusev
2026-06-21 15:22:13 +08:00
committed by Xubin Ren
co-authored by Claude Opus 4.8
parent a5768a4ebe
commit 74daa81a1b
3 changed files with 26 additions and 26 deletions
+14 -14
View File
@@ -131,12 +131,11 @@ async def test_tavily_search(monkeypatch):
assert "https://openclaw.io" in result
def test_keenable_without_api_key_is_treated_as_duckduckgo(monkeypatch):
# The REST API requires a key; without one we fall back to DuckDuckGo.
def test_keenable_without_api_key_is_concurrency_safe(monkeypatch):
monkeypatch.delenv("KEENABLE_API_KEY", raising=False)
tool = _tool(provider="keenable", api_key="")
assert tool.exclusive is True
assert tool.concurrency_safe is False
assert tool.exclusive is False
assert tool.concurrency_safe is True
@pytest.mark.asyncio
@@ -159,20 +158,21 @@ async def test_keenable_search(monkeypatch):
@pytest.mark.asyncio
async def test_keenable_fallback_to_duckduckgo_when_no_key(monkeypatch):
class MockDDGS:
def __init__(self, **kw):
pass
async def test_keenable_without_api_key_uses_public_endpoint(monkeypatch):
async def mock_post(self, url, **kw):
assert url == "https://api.keenable.ai/v1/search/public"
assert "X-API-Key" not in kw["headers"]
assert kw["headers"]["X-Keenable-Title"] == "nanobot"
return _response(json={
"results": [{"title": "Public", "url": "https://keenable.ai/pub", "description": "ok"}]
})
def text(self, query, max_results=5):
return [{"title": "Fallback", "href": "https://ddg.example", "body": "DuckDuckGo fallback"}]
monkeypatch.setattr("ddgs.DDGS", MockDDGS)
monkeypatch.setattr(httpx.AsyncClient, "post", mock_post)
monkeypatch.delenv("KEENABLE_API_KEY", raising=False)
tool = _tool(provider="keenable", api_key="")
result = await tool.execute(query="keenable", count=1)
assert "DuckDuckGo fallback" in result
assert "Public" in result
assert "https://keenable.ai/pub" in result
@pytest.mark.asyncio