opencode-search-plugin 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,41 +6,60 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/opencode-search-plugin.svg)](https://www.npmjs.com/package/opencode-search-plugin)
7
7
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
8
 
9
- **Web search for OpenCode that works out of the box, no API key needed.**
9
+ **Web search for OpenCode that works out of the box, no API key required.**
10
10
 
11
- One command adds thirteen search providers to OpenCode v2, growing the pool from five to eighteen. AnySearch and Keenable work with no setup at all; the other eleven only activate once you add a key.
11
+ AnySearch and Keenable work immediately without any configuration. Other providers activate only when an API key is configured, ensuring you are not billed for unused services.
12
+
13
+ When OpenCode operates in `random` mode, it automatically fails over to the next available provider if one encounters an HTTP 429 rate limit. Providers without configured keys are excluded from the pool, preventing requests to unconfigured services.
12
14
 
13
15
  ```sh
14
16
  opencode plugin add opencode-search-plugin
15
17
  ```
16
18
 
17
- Install it, then just ask OpenCode something that needs the web.
19
+ After installation, ask OpenCode any question that requires a web search.
20
+
21
+ ## Skip the provider prompt
22
+
23
+ On your first search, OpenCode prompts you to select a search provider. Set `websearch.provider` in your configuration to bypass this prompt:
24
+
25
+ ```jsonc
26
+ {
27
+ "$schema": "https://opencode.ai/config.json",
28
+ "websearch": { "provider": "random" }
29
+ }
30
+ ```
31
+
32
+ `"random"` uses one provider per session and automatically fails over to another if rate-limited. You can also pin a specific provider using its ID (such as `"osp-brave"`), or disable web search with `false`.
18
33
 
19
- <!-- TODO: replace this comment with a 10-second GIF once you record one:
20
- ask a question → the websearch tool fires → results come back -->
34
+ <!-- TODO: replace this comment with a 10-second GIF:
35
+ set websearch.provider -> ask a question -> provider responds with sources -->
21
36
 
22
- Thirteen providers in one package, use whichever you like:
37
+ ## Supported providers
23
38
 
24
- [AnySearch](https://www.anysearch.com/) · [Bocha](https://bocha.ai/) · [Brave Search](https://brave.com/search/api/) · [DeepSeek](https://api-docs.deepseek.com/guides/search/) · [Jina](https://jina.ai/) · [Keenable](https://keenable.ai/) · [Linkup](https://www.linkup.so/) · [Metaso](https://metaso.cn/) · [Ollama](https://ollama.com/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/)
39
+ [AnySearch](https://www.anysearch.com/) · [Bocha](https://bocha.ai/) · [Brave Search](https://brave.com/search/api/) · [DeepSeek](https://api-docs.deepseek.com/guides/search/) · [Jina](https://jina.ai/) · [Keenable](https://keenable.ai/) · [Linkup](https://www.linkup.so/) · [Metaso](https://metaso.cn/) · [Ollama](https://ollama.com/) · [Perplexity](https://www.perplexity.ai/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/) · [Zhipu](https://bigmodel.cn/)
25
40
 
26
41
  ## Why this exists
27
42
 
28
- OpenCode v2 ships with five web search providers built in: `exa`, `firecrawl`, `parallel`, `tavily` and `tinyfish`. Outside that set, a much wider field of search APIs offers free quotas or free tiers, enough to run an agent's searches without paying for the privilege.
43
+ OpenCode v2 ships with several built-in search providers: `exa`, `firecrawl`, `parallel`, `tavily`, and `tinyfish`. In addition, many external search APIs offer free tiers or generous free quotas.
44
+
45
+ This plugin integrates those providers into OpenCode:
29
46
 
30
- This repository adds plugins for thirteen more of them, taking the pool from five providers to eighteen. AnySearch and Keenable run without a key at all; the other eleven bind only when you supply one, so installing this package never routes a query to a provider that would immediately fail. Which of them are worth your time depends on the free tier each one currently offers, so check a provider's pricing before you rely on it.
47
+ - AnySearch and Keenable work immediately without an API key or account.
48
+ - Multiple providers offer free tiers, making it practical to maintain a low-cost backup pool.
49
+ - Expands OpenCode's `random` mode pool: when a provider returns an HTTP 429, OpenCode retries with another to prevent search interruptions.
31
50
 
32
51
  ## Install
33
52
 
34
- Three equivalent ways to install; pick one. **npm is recommended**: a version published to the registry is immutable, so a pinned `@3.1.0` always installs the same bytes, whereas a GitHub tag can in principle be moved by a maintainer to point at a different commit. The first two install the same complete package (all thirteen); use the third for one or two files only.
53
+ Choose your preferred installation method. **npm is recommended**: published versions are immutable, so pinning with `@<version>` guarantees a consistent build.
35
54
 
36
55
  ### 1. From npm (recommended)
37
56
 
38
57
  ```sh
39
- # add @3.1.0 to pin
58
+ # Append @<version> to pin a specific version
40
59
  opencode plugin add opencode-search-plugin
41
60
  ```
42
61
 
43
- or declare it in `opencode.json(c)`:
62
+ Or declare it in `opencode.json(c)`:
44
63
 
45
64
  ```jsonc
46
65
  {
@@ -52,11 +71,11 @@ or declare it in `opencode.json(c)`:
52
71
  ### 2. From GitHub
53
72
 
54
73
  ```sh
55
- # add #v3.1.0 to pin
74
+ # Append #v<version> to pin a specific tag
56
75
  opencode plugin add github:guotree/opencode-search-plugin
57
76
  ```
58
77
 
59
- or declare it in `opencode.json(c)`:
78
+ Or declare it in `opencode.json(c)`:
60
79
 
61
80
  ```jsonc
62
81
  {
@@ -67,24 +86,24 @@ or declare it in `opencode.json(c)`:
67
86
 
68
87
  ### 3. By copying a file
69
88
 
70
- Every plugin is a single `.ts` file with no relative imports and no build step. Drop it where OpenCode auto-discovers plugins:
89
+ Each provider plugin is a self-contained `.ts` file with no relative imports and no build step. Place it in one of OpenCode's auto-discovery directories:
71
90
 
72
91
  ```text
73
92
  ~/.config/opencode/plugins/ # global
74
- <project>/.opencode/plugins/ # one project
93
+ <project>/.opencode/plugins/ # project-scoped
75
94
  ```
76
95
 
77
- For a location that is not auto-discovered, list it explicitly:
96
+ For directories outside the auto-discovery paths, specify the file path explicitly:
78
97
 
79
98
  ```jsonc
80
99
  { "plugins": ["./plugins/brave-search.ts"] }
81
100
  ```
82
101
 
83
- > Do not install the package *and* copy its files. Both register the same websearch provider IDs, so the pool would contain each service twice.
102
+ > Do not install the package *and* copy individual plugin files. Both register the same provider IDs, which will duplicate entries in the search pool.
84
103
 
85
- ### Skip the services you have no key for
104
+ ### Skip providers you don't use
86
105
 
87
- Loading the package registers all thirteen, but a provider without a key never joins the search pool; see [Keys](#keys). To keep some out of the registry entirely, use either or both of these optional options:
106
+ Installing the package registers all providers, but providers without configured API keys remain inactive (see [Keys](#keys)). To completely exclude or restrict providers, use `disable` or `enable`:
88
107
 
89
108
  ```jsonc
90
109
  {
@@ -97,201 +116,205 @@ Loading the package registers all thirteen, but a provider without a key never j
97
116
  }
98
117
  ```
99
118
 
100
- - `disable` is a deny-list: it never registers the IDs it names (the provider IDs in the table below). AnySearch and Keenable need no key, so `disable` is the only way to keep those two out of the pool.
101
- - `enable` is an allow-list: only the IDs it names are registered, and everything else is skipped. `"enable": ["osp-brave", "osp-jina"]` runs those two alone.
119
+ - `disable`: Deny-list of provider IDs (from the table below). Because AnySearch and Keenable require no key, `disable` is the only way to remove them from the pool.
120
+ - `enable`: Allow-list of provider IDs. Only the listed IDs are registered. For example, `"enable": ["osp-brave", "osp-jina"]` activates only those two.
102
121
 
103
- When both are set, **`disable` wins on any overlap**. A per-provider exclusion is never overridden by a bulk allow-list:
122
+ If both are specified, **`disable` takes precedence on any overlap**:
104
123
 
105
124
  ```jsonc
106
125
  {
107
126
  "options": {
108
127
  "enable": ["osp-brave", "osp-jina", "osp-serper"],
109
128
  "disable": ["osp-serper", "osp-youcom"]
110
- // osp-serper stays out: disable overrides enable.
111
- // osp-youcom was never allowed in; that entry is inert.
129
+ // osp-serper is excluded: disable overrides enable.
130
+ // osp-youcom was not in enable, so it is safely ignored.
112
131
  }
113
132
  }
114
133
  ```
115
134
 
116
- Read as set arithmetic, the registered pool is `enable − disable`. The overlap drops out; a `disable` ID the allow-list never contained was never a candidate to begin with, redundant but harmless and unreported. The overlap is the only redundancy that earns a warning, and the log names the IDs in it.
117
-
118
- ```text
119
- enable = {osp-brave, osp-jina, osp-serper, osp-keenable, osp-linkup}
120
- disable = {osp-keenable, osp-linkup, osp-youcom}
121
- pool = osp-brave, osp-jina -- enable minus disable
122
- ```
123
-
124
- Configuration that cannot take effect is reported in the log rather than silently dropped: an ID this package does not register (a typo) is named, as is an option that is not an array. An `enable` list that ends up naming no known provider leaves the pool empty; the log says so.
135
+ The active set is computed as `enable - disable`. Overlapping IDs are dropped, and invalid or unrecognized IDs produce a warning in the logs rather than failing silently.
125
136
 
126
137
  ## Upgrade
127
138
 
128
139
  ```sh
129
- # installed from npm
140
+ # Installed from npm
130
141
  opencode plugin update opencode-search-plugin
131
- # installed from GitHub
142
+ # Installed from GitHub
132
143
  opencode plugin update github:guotree/opencode-search-plugin
133
- # everything outdated
144
+ # Update all outdated plugins
134
145
  opencode plugin update
135
146
  ```
136
147
 
137
- The target is the specifier that was configured: an npm install is named by its package name, a GitHub install answers only to its `github:` form; naming it by its bare package name fails with `Plugin is not configured`. Run `opencode plugin check` first to see what is outdated. An install without a pinned version follows new releases; a pinned one (`@3.1.0` on npm, `#v3.1.0` on GitHub) stays on its pin until you re-run `plugin add` with a newer pin or edit the specifier in `opencode.json(c)`. A locally copied file is upgraded by copying the new version over the old one.
148
+ The update target must match the configured plugin specifier:
149
+ - For npm installations, use the package name (`opencode-search-plugin`).
150
+ - For GitHub installations, use the full `github:` specifier (using a bare package name will return `Plugin is not configured`).
151
+
152
+ Run `opencode plugin check` first to inspect available updates. Unpinned installations update to the latest release. Pinned installations (`@<version>` on npm, `#v<version>` on GitHub) remain at their pinned version until you edit the specifier in `opencode.json(c)` or run `plugin add` with a new version. For locally copied files, replace the existing `.ts` file with the newer version.
138
153
 
139
154
  ## Uninstall
140
155
 
141
156
  ```sh
142
- # installed from npm
157
+ # Installed from npm
143
158
  opencode plugin remove opencode-search-plugin
144
- # installed from GitHub
159
+ # Installed from GitHub
145
160
  opencode plugin remove github:guotree/opencode-search-plugin
146
161
  ```
147
162
 
148
- The same rule: pass the specifier you installed with. The command edits the global configuration; if the plugin is instead a `plugins` entry in `opencode.json(c)` or a locally copied file, delete the entry or the copied `.ts` file. Saved `/connect` credentials are untouched; a reinstall finds them again. To turn off a few providers instead of the whole plugin, use `enable`/`disable` above.
163
+ Pass the same specifier used during installation. This command updates global configuration. If the plugin was configured as an entry in `opencode.json(c)` or as a local file, remove the configuration entry or delete the copied `.ts` file. Saved `/connect` credentials are preserved and will be recognized upon reinstallation. To deactivate specific providers without removing the entire plugin, use `enable` / `disable` as described above.
149
164
 
150
165
  ## Keys
151
166
 
152
- Every provider except AnySearch and Keenable needs an API key. Give it one of two ways:
167
+ Every provider except AnySearch and Keenable requires an API key. You can provide credentials in one of two ways:
153
168
 
154
- - **`/connect`** inside OpenCode: stored per provider, rotated without a restart, survives environment changes.
155
- - **Environment variable**: read as a fallback, so it works in headless and CI contexts.
169
+ - **`/connect` in OpenCode**: Stored per provider, rotatable without restarting OpenCode, and unaffected by environment changes.
170
+ - **Environment variables**: Read as a fallback for headless, automated, or CI environments.
156
171
 
157
- Environment variables are namespaced `OSP_<PROVIDER>_API_KEY` (say `OSP_BRAVE_API_KEY`), so the key this plugin reads never collides with the same vendor name exported for another tool. A credential stored in `/connect` outranks the environment either way.
172
+ Environment variables use the `OSP_<PROVIDER>_API_KEY` namespace (e.g., `OSP_BRAVE_API_KEY`) to prevent naming collisions with other tools that export vendor variables. Credentials stored in `/connect` always take precedence over environment variables.
158
173
 
159
- A provider with no key stays unbound: it is absent from the websearch pool, so `"provider": "random"` can never route a query to a service that would immediately fail.
174
+ A provider without a configured key remains unbound: it is omitted from the search pool, ensuring `"provider": "random"` never routes queries to a service that would immediately fail.
160
175
 
161
- AnySearch and Keenable are the exceptions. Both have an anonymous tier, so they are always registered and only attach their credential header when a key exists. For Keenable the two tiers draw on separate budgets: a key spends the free allowance of 100,000 requests a month, while a keyless call goes to the `/public` endpoint instead and is metered by rate: 1,000 requests an hour and 10 a second per IP, on a pool shared with everyone else on your egress address. A key is what makes that shared headroom irrelevant.
176
+ AnySearch and Keenable are exceptions. Both include an anonymous tier and are registered by default, attaching credentials only when a key is provided. For Keenable, the two tiers use separate quotas: configuring a key provides 100,000 free requests per month, whereas keyless requests route to the `/public` endpoint and are rate-limited to 1,000 requests/hour and 10 requests/second per IP (shared across all users on that egress address). Configuring an API key removes dependence on that shared IP rate limit.
162
177
 
163
- Once keys are in place, pick how OpenCode chooses among them:
164
-
165
- ```jsonc
166
- { "websearch": { "provider": "random" } }
167
- ```
168
-
169
- `random` spreads queries across every bound provider. Set it to a specific ID such as `"osp-brave"` to pin one, or `false` to turn web search off.
178
+ With keys configured, `websearch.provider` controls routing: `"random"` distributes queries across all bound providers (see [Skip the provider prompt](#skip-the-provider-prompt)), a specific ID such as `"osp-brave"` pins that provider, and `false` disables web search entirely.
170
179
 
171
180
  ## Providers
172
181
 
173
- Every ID below carries an `osp-` prefix; it is this package's namespace. OpenCode's own providers register bare vendor names (`exa`, `tavily`), and a vendor's official plugin would do the same, so the prefix is what keeps those entries from colliding with ours, both in the websearch pool and in the `/connect` table, where credentials are keyed by integration ID. Use the prefixed form verbatim in `websearch.provider` and in `enable`/`disable`.
182
+ Each provider ID carries an `osp-` prefix to prevent name collisions with OpenCode's built-in providers or official vendor plugins (which use bare names like `exa` or `tavily`). Use the prefixed ID in `websearch.provider` and in `enable` / `disable` options.
174
183
 
175
- The namespace also shows on the display name: `/connect` lists each of this package's entries with an `(osp)` marker (`DeepSeek (osp)` beside a built-in `DeepSeek`), so a key saved here cannot be mistaken for a built-in provider's. The websearch pool keeps the plain provider name, since the pool holds only search providers and the collision does not exist there.
184
+ In OpenCode's `/connect` settings, each provider is labeled with an `(osp)` suffix (e.g., `DeepSeek (osp)`) to distinguish it from built-in integrations. The search pool itself displays the clean provider name without the suffix.
176
185
 
177
186
  | Provider | ID | Key (env / `/connect`) | Anonymous | Notes |
178
187
  | --- | --- | --- | --- | --- |
179
- | AnySearch | `osp-anysearch` | `OSP_ANYSEARCH_API_KEY` | yes | Free tier per IP; intent router, `cn`/`intl` zones |
180
- | Bocha | `osp-bocha` | `OSP_BOCHA_API_KEY` | no | Bing-compatible results; billed per search, and by the token when a summary is asked for |
188
+ | AnySearch | `osp-anysearch` | `OSP_ANYSEARCH_API_KEY` | yes | Free tier per IP; intent routing, `cn` / `intl` zones |
189
+ | Bocha | `osp-bocha` | `OSP_BOCHA_API_KEY` | no | Bing-compatible results; billed per search, and by token when summaries are requested |
181
190
  | Brave Search | `osp-brave` | `OSP_BRAVE_API_KEY` | no | Independent index, 20 results max |
182
- | DeepSeek | `osp-deepseek` | `OSP_DEEPSEEK_API_KEY` | no | In-house index; $0.75 per 1,000 successful requests; 5 QPS cap |
183
- | Jina | `osp-jina` | `OSP_JINA_API_KEY` | no | Page content is opt-in because it is token-billed |
184
- | Keenable | `osp-keenable` | `OSP_KEENABLE_API_KEY` | yes | 100,000 requests a month with a key; keyless tier is per IP |
185
- | Linkup | `osp-linkup` | `OSP_LINKUP_API_KEY` | no | `flash`/`fast`/`standard`/`deep` reasoning depth |
191
+ | DeepSeek | `osp-deepseek` | `OSP_DEEPSEEK_API_KEY` | no | In-house index; $0.75 per 1,000 successful requests; 5 QPS rate limit |
192
+ | Jina | `osp-jina` | `OSP_JINA_API_KEY` | no | Full-page content is opt-in due to token-based billing |
193
+ | Keenable | `osp-keenable` | `OSP_KEENABLE_API_KEY` | yes | 100,000 free requests/month with a key; keyless tier is rate-limited per IP |
194
+ | Linkup | `osp-linkup` | `OSP_LINKUP_API_KEY` | no | Four reasoning depths: `flash`, `fast`, `standard`, `deep` |
186
195
  | Metaso | `osp-metaso` | `OSP_METASO_API_KEY` | no | Billed per search, not per result |
187
- | Ollama | `osp-ollama` | `OSP_OLLAMA_API_KEY` | no | Free personal tier; each hit carries the full page as markdown, so results are context-heavy, capped at 10, and carry no dates |
196
+ | Ollama | `osp-ollama` | `OSP_OLLAMA_API_KEY` | no | Free personal tier; returns full-page markdown per result (up to 10 results, no publication dates) |
197
+ | Perplexity | `osp-perplexity` | `OSP_PERPLEXITY_API_KEY` | no | Search API returns direct search results without generated answers; $5.00 / 1,000 requests ($1.00 with `fast`) |
188
198
  | Querit | `osp-querit` | `OSP_QUERIT_API_KEY` | no | Falls back to sentence matches when a snippet is missing |
189
- | SerpApi | `osp-serpapi` | `OSP_SERPAPI_API_KEY` | no | Google via SerpApi; also has its own free 1-hour server cache |
199
+ | SerpApi | `osp-serpapi` | `OSP_SERPAPI_API_KEY` | no | Google via SerpApi; includes SerpApi's free 1-hour server-side cache |
190
200
  | Serper | `osp-serper` | `OSP_SERPER_API_KEY` | no | Google via Serper; billed per query in credits |
191
- | You.com | `osp-youcom` | `OSP_YDC_API_KEY` | no | Web and news merged; extraction modes |
201
+ | You.com | `osp-youcom` | `OSP_YDC_API_KEY` | no | Combined web and news results; multiple extraction modes |
202
+ | Zhipu | `osp-zhipu` | `OSP_ZHIPU_API_KEY` | no | Standalone search endpoint, no model call; billed per search from ¥0.01 (Search-Std), no free tier; four engines including Sogou and Quark; `cn` / `intl` sites both supported |
192
203
 
193
204
  ## Configuration
194
205
 
195
- Knobs are environment variables, read once when OpenCode starts; change one and restart. Defaults match what each API documents, so leaving them unset is the right starting point.
206
+ Configuration options are loaded as environment variables on startup. If you change a setting, restart OpenCode for it to take effect. Default values align with upstream API documentation recommendations.
196
207
 
197
- ### Shared by all thirteen
208
+ ### Shared by every provider
198
209
 
199
- | Variable | Default | Meaning |
210
+ | Variable | Default | Description |
200
211
  | --- | --- | --- |
201
- | `<PROVIDER>_CACHE_TTL` | `600000` | Milliseconds to reuse a response for the same normalized query. `0` disables caching. |
202
- | `<PROVIDER>_TIMEOUT` | `25000` | Milliseconds to wait for the provider before failing. `0` removes the deadline but keeps cancellation. |
212
+ | `<PROVIDER>_CACHE_TTL` | `600000` | Duration in milliseconds to reuse cached responses for identical normalized queries (`0` disables caching) |
213
+ | `<PROVIDER>_TIMEOUT` | `25000` | Timeout in milliseconds before failing a request (`0` removes timeout while preserving cancellation) |
203
214
 
204
215
  ### AnySearch
205
216
 
206
- | Variable | Default | Meaning |
217
+ | Variable | Default | Description |
207
218
  | --- | --- | --- |
208
- | `ANYSEARCH_MAX_RESULTS` | `10` | Results per search, clamped to the API's 1–10 |
209
- | `ANYSEARCH_TAG` | — | Pin the intent router to a sub-domain, e.g. `code.doc` |
210
- | `ANYSEARCH_ZONE` | — | `cn` or `intl` |
211
- | `ANYSEARCH_LANGUAGE` | — | Preferred result language, e.g. `zh-CN` |
212
- | `ANYSEARCH_FORMAT` | `markdown` | `markdown` or `json` for the content field |
213
- | `OSP_ANYSEARCH_API_KEY` | — | Optional; anonymous access works without it |
219
+ | `ANYSEARCH_MAX_RESULTS` | `10` | Results per search, clamped to 1–10 |
220
+ | `ANYSEARCH_TAG` | — | Pins the intent router to a sub-domain, e.g., `code.doc` |
221
+ | `ANYSEARCH_ZONE` | — | Geographic zone: `cn` or `intl` |
222
+ | `ANYSEARCH_LANGUAGE` | — | Preferred result language, e.g., `zh-CN` |
223
+ | `ANYSEARCH_FORMAT` | `markdown` | Content format: `markdown` or `json` |
224
+ | `OSP_ANYSEARCH_API_KEY` | — | Optional; anonymous access is used when omitted |
214
225
 
215
226
  ### Bocha
216
227
 
217
- | Variable | Default | Meaning |
228
+ | Variable | Default | Description |
218
229
  | --- | --- | --- |
219
230
  | `BOCHA_COUNT` | `10` | Results per search, clamped to 1–50 (Bocha's maximum) |
220
- | `BOCHA_FRESHNESS` | — | `noLimit`, `oneDay`, `oneWeek`, `oneMonth`, `oneYear`, a `YYYY-MM-DD` date, or a `YYYY-MM-DD..YYYY-MM-DD` range. Unset omits the parameter, which the docs recommend: Bocha rewrites time ranges itself, and a pinned range often finds nothing within it |
221
- | `BOCHA_SUMMARY` | off | `true` swaps each snippet for a generated summary — more context to read, at the higher per-call price |
231
+ | `BOCHA_FRESHNESS` | — | Freshness filter: `noLimit`, `oneDay`, `oneWeek`, `oneMonth`, `oneYear`, `YYYY-MM-DD`, or `YYYY-MM-DD..YYYY-MM-DD`. Omitting this parameter is recommended by Bocha, as upstream query rewriting may yield empty results for fixed ranges |
232
+ | `BOCHA_SUMMARY` | `off` | `true` replaces snippets with generated summaries (provides more context at higher per-request cost) |
222
233
 
223
234
  ### Brave Search
224
235
 
225
- | Variable | Default | Meaning |
236
+ | Variable | Default | Description |
226
237
  | --- | --- | --- |
227
238
  | `BRAVE_NUM` | `8` | Results per search, clamped to 1–20 (Brave's maximum) |
228
239
  | `BRAVE_COUNTRY` | — | Two-letter country code, or `ALL` |
229
- | `BRAVE_LANG` | — | Result language, e.g. `en`, `zh-hans`, `pt-br` |
230
- | `BRAVE_FRESHNESS` | — | `pd` (24h), `pw` (7d), `pm` (31d), `py` (365d), or `YYYY-MM-DDtoYYYY-MM-DD` |
240
+ | `BRAVE_LANG` | — | Result language code, e.g., `en`, `zh-hans`, `pt-br` |
241
+ | `BRAVE_FRESHNESS` | — | Time filter: `pd` (24h), `pw` (7d), `pm` (31d), `py` (365d), or `YYYY-MM-DDtoYYYY-MM-DD` |
231
242
 
232
243
  ### DeepSeek
233
244
 
234
- | Variable | Default | Meaning |
245
+ | Variable | Default | Description |
235
246
  | --- | --- | --- |
236
- | `DEEPSEEK_COUNT` | `10` | Results per search, clamped to 1–50 (DeepSeek's maximum). Price is per request, so more results cost nothing extra |
247
+ | `DEEPSEEK_COUNT` | `10` | Results per search, clamped to 1–50 (DeepSeek's maximum). Billed per request; additional results incur no extra charge |
237
248
 
238
249
  ### Jina
239
250
 
240
- | Variable | Default | Meaning |
251
+ | Variable | Default | Description |
241
252
  | --- | --- | --- |
242
- | `JINA_NUM` | `0` | Results per search, 1–100. `0` omits the parameter, which keeps Jina's faster and wider result types |
243
- | `JINA_INCLUDE_CONTENT` | off | `true` returns each hit's full page markdown, far more context and billed tokens |
253
+ | `JINA_NUM` | `0` | Results per search (1–100). `0` omits the parameter, preserving Jina's faster and broader result types |
254
+ | `JINA_INCLUDE_CONTENT` | `off` | `true` returns full-page markdown per hit, significantly increasing context and billed tokens |
244
255
  | `JINA_GL` / `JINA_HL` | — | Country / language code |
245
- | `JINA_LOCATION` | — | City-level origin to simulate, e.g. `Berlin` |
246
- | `JINA_TIMEOUT` | `25000`, or `60000` with `JINA_INCLUDE_CONTENT` | Only provider whose deadline varies by mode: fetching whole pages is legitimately slower than a snippet search |
256
+ | `JINA_LOCATION` | — | Simulated city-level location, e.g., `Berlin` |
257
+ | `JINA_TIMEOUT` | `25000` (`60000` with `JINA_INCLUDE_CONTENT`) | Request timeout in milliseconds; full-page fetching uses a higher default timeout |
247
258
 
248
259
  ### Keenable
249
260
 
250
- | Variable | Default | Meaning |
261
+ | Variable | Default | Description |
251
262
  | --- | --- | --- |
252
263
  | `KEENABLE_MAX_RESULTS` | `10` | Results per search, clamped to 1–50 |
253
- | `KEENABLE_MODE` | — | `pro` (deeper retrieval) or `realtime` (fastest); unset leaves Keenable's own default |
254
- | `KEENABLE_TITLE` | `opencode-search-plugin` | App name sent as `X-Keenable-Title`, which the keyless endpoint requires to attribute the call |
255
- | `OSP_KEENABLE_API_KEY` | — | Optional; keyless access works without it |
264
+ | `KEENABLE_MODE` | — | Search mode: `pro` (deeper retrieval) or `realtime` (faster); unset uses Keenable's default |
265
+ | `KEENABLE_TITLE` | `opencode-search-plugin` | Application identifier sent as `X-Keenable-Title`, required by the keyless endpoint for attribution |
266
+ | `OSP_KEENABLE_API_KEY` | — | Optional; keyless access is used when omitted |
256
267
 
257
268
  ### Linkup
258
269
 
259
- | Variable | Default | Meaning |
270
+ | Variable | Default | Description |
260
271
  | --- | --- | --- |
261
- | `LINKUP_DEPTH` | `fast` | `flash`, `fast`, `standard`, or `deep` |
272
+ | `LINKUP_DEPTH` | `fast` | Search depth: `flash`, `fast`, `standard`, or `deep` |
262
273
  | `LINKUP_MAX_RESULTS` | `10` | Results per search, clamped to 1–100 |
263
274
 
264
275
  ### Metaso
265
276
 
266
- | Variable | Default | Meaning |
277
+ | Variable | Default | Description |
267
278
  | --- | --- | --- |
268
279
  | `METASO_SEARCH_SIZE` | `20` | Results per search, clamped to 1–100 |
269
- | `METASO_INCLUDE_RAW_CONTENT` | off | `true` returns every hit's full source text at the same search cost, but far more context tokens |
280
+ | `METASO_INCLUDE_RAW_CONTENT` | `off` | `true` returns full source text per hit without extra search cost, but substantially increases context tokens |
270
281
 
271
282
  ### Ollama
272
283
 
273
- | Variable | Default | Meaning |
284
+ | Variable | Default | Description |
285
+ | --- | --- | --- |
286
+ | `OLLAMA_MAX_RESULTS` | `5` | Results per search, clamped to 1–10 (API maximum). Because each result includes full-page markdown rather than snippets, the default 5 results provide extensive context |
287
+
288
+ ### Perplexity
289
+
290
+ | Variable | Default | Description |
274
291
  | --- | --- | --- |
275
- | `OLLAMA_MAX_RESULTS` | `5` | Results per search, clamped to 1–10 (the API's maximum). `content` is a whole page of markdown rather than a snippet, so even the default 5 results run to thousands of characters of context |
292
+ | `PERPLEXITY_MAX_RESULTS` | `10` | Results per search, clamped to 1–20 (web search supports up to 20; `search_type=people` accepts up to 50) |
293
+ | `PERPLEXITY_SEARCH_TYPE` | — | Search type: `web` (API default, $5.00 / 1,000 requests), `fast` ($1.00 / 1,000 requests, lower latency), or `people`; unset omits the parameter |
294
+ | `PERPLEXITY_RECENCY` | — | Recency filter: `hour`, `day`, `week`, `month`, or `year` |
295
+ | `PERPLEXITY_COUNTRY` | — | Two-letter ISO 3166-1 country code, e.g., `US` |
296
+ | `PERPLEXITY_LANGUAGE` | — | Comma-separated ISO 639-1 language codes, e.g., `en,zh` (maximum of 10 codes sent, adhering to the stricter quickstart limit) |
297
+
298
+ There is intentionally no domain or source filter configuration: the Perplexity API provides no dedicated toggle for news, Reddit, or academic search. Upstream documentation recommends using `search_domain_filter` allowlists per query rather than applying a global configuration.
276
299
 
277
300
  ### Querit
278
301
 
279
- | Variable | Default | Meaning |
302
+ | Variable | Default | Description |
280
303
  | --- | --- | --- |
281
304
  | `QUERIT_COUNT` | `8` | Results per search |
282
305
 
283
306
  ### SerpApi
284
307
 
285
- | Variable | Default | Meaning |
308
+ | Variable | Default | Description |
286
309
  | --- | --- | --- |
287
310
  | `SERPAPI_GL` / `SERPAPI_HL` | — | Country / language code |
288
- | `SERPAPI_TBS` | — | Advanced filter, e.g. `qdr:d` (past day), `qdr:y` (past year) |
311
+ | `SERPAPI_TBS` | — | Advanced search filter, e.g., `qdr:d` (past day), `qdr:y` (past year) |
289
312
 
290
- There is deliberately no result-count knob: Google withdrew the `num` parameter, so SerpApi cannot honour one.
313
+ There is intentionally no result-count configuration: Google deprecated the `num` parameter, so SerpApi no longer supports it.
291
314
 
292
315
  ### Serper
293
316
 
294
- | Variable | Default | Meaning |
317
+ | Variable | Default | Description |
295
318
  | --- | --- | --- |
296
319
  | `SERPER_NUM` | `10` | Results per search, clamped to 1–100 |
297
320
  | `SERPER_GL` / `SERPER_HL` | — | Country / language code |
@@ -299,48 +322,62 @@ There is deliberately no result-count knob: Google withdrew the `num` parameter,
299
322
 
300
323
  ### You.com
301
324
 
302
- | Variable | Default | Meaning |
325
+ | Variable | Default | Description |
303
326
  | --- | --- | --- |
304
327
  | `YOUCOM_COUNT` | `10` | Results per section (web and news each), clamped to 1–100 |
305
- | `YOUCOM_EXTRACTION` | `highlights` | `highlights` returns answering passages, `none` returns bare snippets, `full_page` crawls every hit and bills per live-fetched page |
306
- | `YOUCOM_LANGUAGE` / `YOUCOM_COUNTRY` | — | e.g. `EN` / `US` |
307
- | `YOUCOM_FRESHNESS` | — | `day`, `week`, `month`, `year`, or `YYYY-MM-DDtoYYYY-MM-DD` |
328
+ | `YOUCOM_EXTRACTION` | `highlights` | Extraction mode: `highlights` returns answering passages, `none` returns raw snippets, and `full_page` fetches full pages and bills per fetched page |
329
+ | `YOUCOM_LANGUAGE` / `YOUCOM_COUNTRY` | — | Language / country code, e.g., `EN` / `US` |
330
+ | `YOUCOM_FRESHNESS` | — | Freshness filter: `day`, `week`, `month`, `year`, or `YYYY-MM-DDtoYYYY-MM-DD` |
331
+
332
+ ### Zhipu
333
+
334
+ | Variable | Default | Description |
335
+ | --- | --- | --- |
336
+ | `ZHIPU_COUNT` | `10` | Results per search, clamped to 1–50 (Zhipu's maximum) |
337
+ | `ZHIPU_ENGINE` | `search_std` | Search engine: `search_std` (¥0.01/search), `search_pro` (¥0.03, higher recall), `search_pro_sogou` / `search_pro_quark` (¥0.05 each). On `ZHIPU_SITE=intl` the only documented engine is `search-prime` ($0.01/search), which is also that site's default |
338
+ | `ZHIPU_RECENCY` | — | Time filter: `oneDay`, `oneWeek`, `oneMonth`, `oneYear`, or `noLimit`. Omitting it leaves the API's `noLimit` default in place |
339
+ | `ZHIPU_CONTENT_SIZE` | — | Excerpt length: `medium` (the API default) or `high`, which sends fuller excerpts at the same per-search price |
340
+ | `ZHIPU_SITE` | `cn` | Endpoint: `cn` for `open.bigmodel.cn`, `intl` for `api.z.ai`. The two are separate account systems -- keys, engines and prices are not interchangeable |
341
+
342
+ The default `search_std` is the cheapest engine; `search_pro` costs three times as much for higher recall, and the Sogou / Quark engines trade a higher price for their own source pools (Sogou covers the Tencent ecosystem and Zhihu; Quark skews recent). Every dispatch is billed -- there is no free tier -- so repeat queries ride the shared cache. Queries are cut to the API's 70-character `search_query` ceiling before dispatch, `count` is omitted on `search_pro_quark` (whose spec row leaves the field out), and on `search_pro_sogou` it is snapped to the nearest of 10/20/30/40/50.
308
343
 
309
344
  ## How a query flows
310
345
 
311
- 1. OpenCode asks the selected provider for the query.
312
- 2. The normalized query (trimmed, whitespace collapsed) is checked against an in-memory cache. A hit returns immediately; repeat queries are free and instant.
313
- 3. On a miss the key is resolved again, so a key rotated in `/connect` applies to the very next search.
314
- 4. The request goes out under a deadline (`<PROVIDER>_TIMEOUT`, default 25s). OpenCode applies no timeout of its own around a search, so without this a provider that accepted the connection and then went silent would hang the whole turn. Failures surface the HTTP status and response body, or name the timeout when that is what happened.
315
- 5. The response is schema-decoded, mapped to `{ url, title, content, time }`, and cached.
346
+ 1. OpenCode issues a search request to the active provider.
347
+ 2. The normalized query (trimmed of leading/trailing whitespace with internal whitespace collapsed) is checked against an in-memory cache. Cache hits return immediately without an external API request.
348
+ 3. On a cache miss, the API key is freshly resolved so that keys rotated in `/connect` take effect immediately.
349
+ 4. The request is dispatched with an abort timeout (`<PROVIDER>_TIMEOUT`, default 25s) to prevent unresponsive upstreams from hanging the session.
350
+ 5. The response is schema-validated, mapped to `{ url, title, content, time }`, and cached for subsequent identical queries.
316
351
 
317
- Dates are parsed from relative phrases (`3 hours ago`, `3小时前`), absolute formats (`Sep 30, 2026`, `31 Mar 2026`), and ISO 8601. Strings whose lenient parsing would invent a wrong year (`Nov 1`, which `Date.parse` reads as 2001) are dropped rather than reported as a bogus timestamp.
352
+ Publication dates are parsed from relative expressions (`3 hours ago`, `3小时前`), absolute dates (`Sep 30, 2026`, `31 Mar 2026`), and ISO 8601 timestamps. Incomplete date strings that could cause lenient parsers to infer an incorrect year (such as `Nov 1`) are ignored.
318
353
 
319
354
  ## Troubleshooting
320
355
 
321
356
  ```sh
322
- # Is the plugin loaded, and did it fail?
357
+ # Check if the plugin is loaded and view its status
323
358
  opencode plugin list
324
359
 
325
- # Which providers are actually in the pool?
326
- # A provider missing here has no key, or `enable`/`disable` kept it out.
327
- # AnySearch and Keenable are always present, since neither needs a key.
360
+ # List providers currently active in the search pool
361
+ # A provider omitted here either lacks an API key or was excluded by enable/disable
362
+ # AnySearch and Keenable are always included since neither requires a key
328
363
  opencode api get "/api/websearch/provider"
329
364
  ```
330
365
 
331
- Also worth checking:
366
+ Common issues to check:
332
367
 
333
- - **A provider is missing from the pool**: it has no key, or `disable` (or a narrow `enable`) filtered it out. Run `/connect` or export its variable, then restart; credential changes made in `/connect` rebind without one. A name that should be there but is not is often a typo: the log flags unknown IDs and any `enable`/`disable` overlap.
334
- - **A search fails with `Keenable rate limited the request (HTTP 429)`**: the keyless pool is capped at 1,000 requests an hour and 10 a second per IP, shared with the rest of your egress address. Set `OSP_KEENABLE_API_KEY` to draw on your own allowance instead.
335
- - **A search fails with `Ollama API error 401`**: `OSP_OLLAMA_API_KEY` must be an ollama.com account key from [ollama.com/settings/keys](https://ollama.com/settings/keys). If it instead holds a bearer token for an Ollama-compatible proxy (a documented OpenCode config shape), store the real key in `/connect` for **Ollama Search (osp)** — a /connect credential outranks the environment.
336
- - **A search fails with `Bocha balance is exhausted (HTTP 403)`**: the account balance is spent; top up at [open.bocha.cn](https://open.bocha.cn). The message carries Bocha's own `message` text and a `log_id` to quote to their support.
337
- - **A search fails with `Bocha rate limited the request (HTTP 429)`**: the per-account request cap scales with the total recharge amount, so a small balance means a small ceiling. The report includes a `retry after Ns` hint when Bocha sent one.
338
- - **A search fails with `Connect X in /connect, or set OSP_X_API_KEY`**: the key disappeared between binding and searching. Reconnect or re-export.
339
- - **A search fails with `X search timed out after 25000ms`**: the provider accepted the request and never answered within the deadline. Retrying usually works; if a provider is consistently slow for you, raise its `<PROVIDER>_TIMEOUT`. This is a deliberate bound: without one a stalled provider hangs the whole turn, because OpenCode applies no timeout of its own around a search.
340
- - **Everything is slow or burning credits**: leave `*_CACHE_TTL` at its default. Repeated queries within the window are served from memory.
341
- - **Nothing happens after editing a plugin file**: OpenCode watches plugins and reloads them; if it does not, `opencode service restart`.
368
+ - **Provider is missing from the search pool**: The provider lacks an API key or was excluded by `disable` (or omitted from `enable`). Configure the key in `/connect` or export its environment variable and restart OpenCode (credentials updated in `/connect` rebind immediately without restarting). If a configured provider is missing, check the logs for typos: unrecognized IDs and conflicts between `enable` and `disable` are flagged on startup.
369
+ - **Search fails with `Keenable rate limited the request (HTTP 429)`**: The keyless tier is limited to 1,000 requests/hour and 10 requests/second per IP, shared across all users on that egress address. Configure `OSP_KEENABLE_API_KEY` to use your dedicated free monthly quota instead.
370
+ - **Search fails with `Ollama API error 401`**: `OSP_OLLAMA_API_KEY` must be an ollama.com account key from [ollama.com/settings/keys](https://ollama.com/settings/keys). If your environment variable holds a bearer token for a local or custom Ollama proxy, configure the API key in `/connect` under **Ollama Search (osp)** instead, which takes precedence over the environment variable.
371
+ - **Search fails with `Bocha balance is exhausted (HTTP 403)`**: The account balance is exhausted; recharge at [open.bocha.cn](https://open.bocha.cn). The error message includes Bocha's response message and a `log_id` for contacting support.
372
+ - **Search fails with `Bocha rate limited the request (HTTP 429)`**: Bocha's per-account request limit scales with total recharge amount. When returned by Bocha, a `retry after Ns` hint is included in the error message.
373
+ - **Search fails with `Zhipu rejected the API key (HTTP 401)`**: The key most likely belongs to the other site. The `cn` site ([open.bigmodel.cn](https://open.bigmodel.cn), `ZHIPU_SITE=cn`) and the `intl` site ([api.z.ai](https://api.z.ai), `ZHIPU_SITE=intl`) are separate account systems whose keys are not interchangeable. Set `ZHIPU_SITE` to match where the key was issued, or get a key from [bigmodel.cn](https://bigmodel.cn/usercenter/proj-mgmt/apikeys).
374
+ - **Search fails with `Zhipu rate limited the request (HTTP 429)`**: Concurrent searches are capped per account, reported by the API as code 1701. Zhipu attaches a `retry after Ns` hint when it has one; waiting it out or running fewer searches at once clears the refusal.
375
+ - **Search fails with `Connect X in /connect, or set OSP_X_API_KEY`**: The credentials were removed or became unavailable between provider registration and search execution. Reconfigure the key in `/connect` or export the environment variable.
376
+ - **Search fails with `X search timed out after 25000ms`**: The upstream provider did not respond within the deadline. Retrying often succeeds. If a provider is consistently slow in your network environment, increase its `<PROVIDER>_TIMEOUT`. This deadline prevents a hanging upstream connection from blocking the entire session, as OpenCode does not enforce its own search timeout.
377
+ - **Queries are slow or consuming excessive credits**: Keep `*_CACHE_TTL` at its default value. Identical queries within the cache TTL window are served from memory without network calls or credit consumption.
378
+ - **Changes to a local plugin file do not take effect**: OpenCode watches plugins and reloads them automatically. If changes are not picked up, run `opencode service restart`.
342
379
 
343
- Logs live at `~/.local/share/opencode/log/opencode.log`; filter for `loading plugin` to see every plugin OpenCode resolved, with its entrypoint. Warnings this plugin emits about `enable`/`disable` land in the same file, carrying a `[opencode-search-plugin]` prefix.
380
+ Logs are located at `~/.local/share/opencode/log/opencode.log`. Filter for `loading plugin` to inspect each resolved plugin and its entry point. Plugin warnings (such as `enable` / `disable` conflicts) are written to this file with an `[opencode-search-plugin]` prefix.
344
381
 
345
382
  ## Development
346
383
 
@@ -349,12 +386,12 @@ npm install
349
386
  npm run check # typecheck + tests
350
387
  ```
351
388
 
352
- - `npm run typecheck`: strict TypeScript across every plugin and test.
353
- - `npm test`: the shared provider contract, per-provider behaviour, date parsing, aggregate registration, and repository-structure guards.
389
+ - `npm run typecheck`: Strict TypeScript checking across all plugins and tests.
390
+ - `npm test`: Runs the shared provider contract, individual provider behaviors, date parsing, aggregate registration, and repository structure guards.
354
391
 
355
- `@opencode/plugin` and `effect` are declared as **devDependencies**, not `dependencies`. OpenCode provides both to plugins at runtime; that is also what lets a single `.ts` file run from `~/.config/opencode/plugins/` with no `node_modules` at all. Declaring them as runtime dependencies would make `opencode plugin add` install a 127 MB tree (`effect` alone is 47 MB, and `@opencode/plugin` pulls in `@aws-sdk`, `google-auth-library` and `@opentelemetry`) to reach 100 KB of plugin source; as devDependencies they are present for typechecking and tests, and absent from a consumer's install.
392
+ `@opencode/plugin` and `effect` are declared as **devDependencies**, not `dependencies`. OpenCode provides both packages at runtime, which allows standalone `.ts` files to run directly from `~/.config/opencode/plugins/` without a local `node_modules` directory. Declaring them as runtime dependencies would cause `opencode plugin add` to download a 127 MB dependency tree (`effect` alone is 47 MB, and `@opencode/plugin` pulls in `@aws-sdk`, `google-auth-library`, and `@opentelemetry`) for 100 KB of plugin source code. Declaring them as devDependencies keeps them available for typechecking and testing while omitting them from consumer installations.
356
393
 
357
- The structure tests exist because each file must remain independently copyable: they fail if a plugin starts importing a sibling module, if the shared blocks stop being byte-identical copies, or if a provider's IDs drift apart. See [CONTRIBUTING.md](CONTRIBUTING.md).
394
+ Structure tests enforce that each plugin file remains independently copyable. They will fail if a plugin imports a sibling module, if shared blocks diverge from their canonical byte-for-byte implementation, or if provider IDs become inconsistent. See [CONTRIBUTING.md](CONTRIBUTING.md).
358
395
 
359
396
  ## License
360
397