Bundle
dsh-web-tavily
Tavily-backed search provider for dsh: registers the `tavily` provider on ctx.web so the web_search tool runs against the Tavily Search API
- Source
- VviLliAm-qwq
- License
- MIT
- Updated
- Updated 18 hours ago
Readme
# dsh-web-tavily
[](https://github.com/VviLliAm-qwq/dsh-web-tavily/actions/workflows/ci.yml)
**English** · [中文](README.zh.md)
Built for [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI).
A host-plane Cordis plugin for dsh: it registers the **`tavily`** search provider
on dsh's web seam (`ctx.web`), so the `web_search` tool goes to the **Tavily
Search API** (`POST https://api.tavily.com/search`).
- It does **not** replace the `web_search` tool itself: the tool, its system
prompt and its result card are untouched — only the search backend changes.
- DeepSeek's native search (`web-search-deepseek`) stays available as a fallback
you can switch to; set `web.searchProvider` back to `deepseek-official` (or
delete the key) to do so.
- The key is always resolved as a **reference** (default `TAVILY_API_KEY`): the
credentials service (`refs:` in `~/.dsh/.credentials.yaml`) first, then the
process environment, and only then a literal `apiKey` in the configuration. The
provider never stores a key.
## How it works
1. The provider (id `tavily`) registers on `ctx.web`; `web.searchProvider: tavily`
makes it the default choice.
2. When `web_search` runs, the seam hands `{ query, maxResults }` to this provider.
3. The provider resolves the key (a snapshot per operation, so one search never
mixes two configuration versions) and calls `{baseURL}/search`.
4. The response is mapped: `results[] → sources[]` (`title` / `url` /
`content→snippet`, plus `published_date → publishedAt` when present), and a
top-level `answer` (Tavily's AI summary) becomes the tool's optional `content`.
5. Cancellation (signal) → `WEB_ABORTED`; an HTTP failure or an unparsable body →
`WEB_PROVIDER_ERROR`; a missing key → `WEB_PROVIDER_CREDENTIAL_MISSING` (the
message names the configuration step).
## Defaults
| Field | Default | Meaning |
| --- | --- | --- |
| `apiKeyEnv` | `TAVILY_API_KEY` | Credential reference (environment-variable name). |
| `apiKey` | (none) | A literal key, only when you do not want a reference. |
| `baseURL` | `https://api.tavily.com` | Overridable with `TAVILY_BASE_URL`. |
| `searchDepth` | `basic` | `basic` / `advanced` (deeper, higher credit use). |
| `topic` | `general` | `general` / `news`. |
| `includeAnswer` | `true` | Ask for Tavily's AI summary (mapped to `content`). |
| `maxResults` | `10` | Fallback cap; `web_search` passes 8 per call, Tavily's ceiling is 20. |
## Install
1. Install it into your profile — it is published on npm as `dsh-web-tavily`:
```sh
dsh plugin --profile dsh-tui add dsh-web-tavily
# from a local checkout instead:
# dsh plugin --profile dsh-tui add file:<path to this package>
```
2. **Make tavily the default search backend.** This has to be written in the
profile's user patch layer (the `web:` section of `settings.yaml` does not
affect service configuration) — edit
`~/.dsh/profiles/dsh-tui/cordis.patch.yml` and add:
```yaml
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: tavily
fetchProvider: http-trusted
```
The `web` row belongs to the `dsh-base` layer, so **do not add a new row with
`insert:`** — the duplicate id crashes the boot with
`duplicate loader entry id: web`. Override the existing row by id instead, and
note that an override **replaces the whole config**, which is why
`fetchProvider` is restated above.
3. Restart dsh-tui (`/restart`).
## Configuration
The plugin's own section lives in `~/.dsh/settings.yaml` (it also appears in the
settings screen under “Web search Tavily”):
```yaml
dsh-web-tavily:
apiKeyEnv: TAVILY_API_KEY
searchDepth: basic
topic: general
includeAnswer: true
maxResults: 10
```
`TAVILY_BASE_URL` overrides the API origin; the credential reference defaults to
`TAVILY_API_KEY`.
The settings screen renders this namespace as a bilingual card (**Web search
(Tavily)** / 联网搜索(Tavily)) with a description for every field, so the
reference is the one control you normally touch. The key itself never enters the
settings document.
## Where the key lives (local only — never in a repository)
Add it under `refs:` in `~/.dsh/.credentials.yaml` (that file holds machine-local
credentials; do not commit or share it):
```yaml
refs:
TAVILY_API_KEY: 'tvly-...'
```
## Switching back to DeepSeek's native search
Set `web.searchProvider` to `deepseek-official` (or delete the key and let the
seam use the only provider available). The seam does **not** fall back
automatically: a broken Tavily configuration fails loudly rather than quietly
using DeepSeek.
## Proxy-compatible fetch (`http-trusted`)
The plugin also registers an `http-trusted` fetch provider, which works around
`web_fetch` being refused by the official `http` provider's safety preflight in
**fake-IP proxy environments** (Clash and friends resolving domains to
`198.18.0.0/15`): `WEB_BLOCKED_URL: resolves to a non-public IP address`.
- It **reuses the official `@deepseek-ai/dsh-web-fetch-http` transport** and
relaxes exactly one check — the “resolved address must be public” preflight
additionally allows `198.18.0.0/15`, the proxy fake-IP range. Everything else
is unchanged: private, loopback and reserved ranges are still refused
(`WEB_BLOCKED_URL`), IPv6 answers are still dropped, and address pinning,
same-origin redirects, the content-type allowlist, byte/character caps and the
no-credentials cookie rule all stay in place.
- ⚠️ **Security boundary:** SSRF protection moves from “enforced inside the host”
to “trust the local proxy's DNS decision”, so the last word on reachability
belongs to the proxy's routing rules. **Intended for a single-user machine with
a trusted self-hosted proxy**; for shared or public deployments go back to the
official `http` provider (below).
- Addresses that are special by construction (`http://192.168.x.x/` and friends)
are still refused.
```yaml
# the web row in ~/.dsh/profiles/dsh-tui/cordis.patch.yml
config:
searchProvider: tavily
fetchProvider: http-trusted
```
**Rolling back to the official `http`:** set `fetchProvider` back to `http`. The
cost is that `web_fetch` is refused again in a fake-IP proxy environment, in
exchange for the complete safety preflight.
## Troubleshooting
- `duplicate loader entry id: web` on boot — the profile's `cordis.patch.yml`
used `insert:` for a row that already exists; rewrite it as the by-id override
shown above.
- `web_search` answers `WEB_PROVIDER_CONFIGURED_MISSING` — the plugin did not
load: check the bundles list and the patch above.
- `web_fetch` answers `WEB_BLOCKED_URL ... non-public IP` — the proxy fake-IP
preflight refused it: switch to `http-trusted`, or turn the proxy's fake-IP
mode off.
## Limitations
- A missing or invalid Tavily key makes `web_search` fail
(`WEB_PROVIDER_CREDENTIAL_MISSING` / `WEB_PROVIDER_ERROR`); the message names
what to configure.
- `max_results` is clamped to Tavily's ceiling of 20.
- An invalid `searchDepth` / `topic` is an error, never a silent fallback to the
default.
## Publishing
- **Repository**: <https://github.com/VviLliAm-qwq/dsh-web-tavily> (public)
- **Release**: `v*` tags drive `.github/workflows/release.yml`, which publishes to npm through **trusted publishing (OIDC)** — no token is stored in the repository.
## Usage
Once installed and configured, let the model call `web_search` as usual: the
`sources` list it returns (and the optional summary) now comes from Tavily.
## License
MIT — see [LICENSE](LICENSE).
Install
dsh plugin --profile web add github:VviLliAm-qwq/dsh-web-tavily
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-web-tavily from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.