- Extract _set_field_from_choices for the shared pick-and-set tail used by
both the LLM and search provider handlers.
- Replace the single-entry _TYPED_FIELD_HANDLERS registry with a direct
isinstance check in _resolve_field_handler (robust to renames, no
class-name strings).
- Drop a redundant str() in the search default computation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The onboard wizard dispatched field handlers by bare field name, so
WebSearchConfig.provider was hijacked by the LLM-provider handler and
showed LLM providers instead of search engines. Keenable was also never
wired into the CLI wizard when it landed in the WebUI.
- Add a single source of truth for selectable search providers
(SEARCH_PROVIDER_OPTIONS in web.py); WebUI settings now import it.
- Add a WebSearchConfig-aware search-provider picker to the wizard and
resolve handlers by (model type, field name) so the LLM and search
provider fields no longer collide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the _BOCHA_SEARCH_API_URL / _VOLCENGINE_SEARCH_API_URL convention
instead of hardcoding the URL inline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Manual testing against the live API showed the Keenable REST endpoint
(/v1/search) returns 401 without a key — the keyless "free tier" applies
only to the CLI, not the HTTP API. Treat Keenable like every other
key-based provider: fall back to DuckDuckGo when no key is configured,
and drop the now-inaccurate free-tier wording from the docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the bespoke nanobot/<version> User-Agent in favor of self.user_agent
for consistency with every other search provider, and add an
X-Keenable-Title: nanobot header so Keenable can attribute traffic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the Keenable search test consistent with the surrounding mocks
(test_tavily/test_brave) rather than introducing a multi-line outlier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the Keenable search provider in line with the established
multi-file provider pattern so it surfaces everywhere the others do:
- settings_api.py: register in the web-search provider options so it
appears in the WebUI settings dropdown (credential: api_key, optional).
- WebUI provider-brand: add keenable brand entry + brand test.
- docs/configuration.md: provider table row + config example.
- Harden _search_keenable: honor config.timeout and return explicit
messages on HTTP status errors (429 / other), matching peer providers.
- Add env-key and HTTP-error tests; add the websocket settings whitelist
assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keenable is a first-party API, so identify as nanobot/<version> instead
of the shared spoofed-browser User-Agent used by scraping providers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Keenable (https://keenable.ai) as a web_search backend, modeled on
the existing httpx-based providers. Unlike key-gated providers, Keenable
has a no-login free tier, so it resolves to itself even without an API
key instead of falling back to DuckDuckGo; the X-API-Key header is only
sent when a key is configured (config api_key or KEENABLE_API_KEY env).
Maps result snippet (falling back to description) into the shared
content field. Covered by tests for keyed/anonymous search and the
no-fallback concurrency behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>