opencode-search-plugin 3.2.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,21 +6,21 @@
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
- AnySearch and Keenable work with no configuration at all. Other providers only activate when you add an API key, so installing this plugin will not charge you for services you do not use.
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
12
 
13
- OpenCode selects providers using `random` mode: when one hits an HTTP 429 rate limit, it switches to the next available provider. Providers without keys are excluded from the pool, so `random` won't attempt to use an unconfigured service.
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.
14
14
 
15
15
  ```sh
16
16
  opencode plugin add opencode-search-plugin
17
17
  ```
18
18
 
19
- Install the plugin, then ask OpenCode any question that requires a web search.
19
+ After installation, ask OpenCode any question that requires a web search.
20
20
 
21
21
  ## Skip the provider prompt
22
22
 
23
- OpenCode asks to pick a search provider on your first search. You can set `websearch.provider` in your config to bypass this prompt:
23
+ On your first search, OpenCode prompts you to select a search provider. Set `websearch.provider` in your configuration to bypass this prompt:
24
24
 
25
25
  ```jsonc
26
26
  {
@@ -29,37 +29,37 @@ OpenCode asks to pick a search provider on your first search. You can set `webse
29
29
  }
30
30
  ```
31
31
 
32
- `"random"` uses one provider per session, and automatically falls back to another if it gets rate-limited. You can also pin a specific provider using its ID (like `"osp-brave"`), or disable search with `false`.
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`.
33
33
 
34
34
  <!-- TODO: replace this comment with a 10-second GIF:
35
35
  set websearch.provider -> ask a question -> provider responds with sources -->
36
36
 
37
37
  ## Supported providers
38
38
 
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/)
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/)
40
40
 
41
41
  ## Why this exists
42
42
 
43
- OpenCode v2 ships with built-in search providers: `exa`, `firecrawl`, `parallel`, `tavily`, and `tinyfish`. However, many external search APIs offer free tiers or generous free quotas.
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
44
 
45
- This plugin adds more providers to your pool:
45
+ This plugin integrates those providers into OpenCode:
46
46
 
47
47
  - AnySearch and Keenable work immediately without an API key or account.
48
- - Several providers offer free tiers, making it practical to maintain a backup pool.
49
- - OpenCode's `random` mode needs multiple providers to handle rate limits effectively. If one provider returns an HTTP 429, OpenCode retries with another.
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.
50
50
 
51
51
  ## Install
52
52
 
53
- Pick whichever installation method you prefer. **npm is recommended**: published versions are immutable, so pinning with `@<version>` always gets you the exact same build.
53
+ Choose your preferred installation method. **npm is recommended**: published versions are immutable, so pinning with `@<version>` guarantees a consistent build.
54
54
 
55
55
  ### 1. From npm (recommended)
56
56
 
57
57
  ```sh
58
- # add @<version> to pin
58
+ # Append @<version> to pin a specific version
59
59
  opencode plugin add opencode-search-plugin
60
60
  ```
61
61
 
62
- or declare it in `opencode.json(c)`:
62
+ Or declare it in `opencode.json(c)`:
63
63
 
64
64
  ```jsonc
65
65
  {
@@ -71,11 +71,11 @@ or declare it in `opencode.json(c)`:
71
71
  ### 2. From GitHub
72
72
 
73
73
  ```sh
74
- # add #v<version> to pin
74
+ # Append #v<version> to pin a specific tag
75
75
  opencode plugin add github:guotree/opencode-search-plugin
76
76
  ```
77
77
 
78
- or declare it in `opencode.json(c)`:
78
+ Or declare it in `opencode.json(c)`:
79
79
 
80
80
  ```jsonc
81
81
  {
@@ -86,24 +86,24 @@ or declare it in `opencode.json(c)`:
86
86
 
87
87
  ### 3. By copying a file
88
88
 
89
- 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:
90
90
 
91
91
  ```text
92
92
  ~/.config/opencode/plugins/ # global
93
- <project>/.opencode/plugins/ # one project
93
+ <project>/.opencode/plugins/ # project-scoped
94
94
  ```
95
95
 
96
- For a location that is not auto-discovered, list it explicitly:
96
+ For directories outside the auto-discovery paths, specify the file path explicitly:
97
97
 
98
98
  ```jsonc
99
99
  { "plugins": ["./plugins/brave-search.ts"] }
100
100
  ```
101
101
 
102
- > 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.
103
103
 
104
104
  ### Skip providers you don't use
105
105
 
106
- Installing the package registers all providers, but any provider without an API key stays inactive; see [Keys](#keys). To exclude specific providers completely, use `disable` or `enable`:
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`:
107
107
 
108
108
  ```jsonc
109
109
  {
@@ -116,200 +116,205 @@ Installing the package registers all providers, but any provider without an API
116
116
  }
117
117
  ```
118
118
 
119
- - `disable`: a deny-list of provider IDs (from the table below). Since AnySearch and Keenable require no key, `disable` is the only way to keep them out of the pool.
120
- - `enable`: an allow-list. Only the listed IDs are registered. For example, `"enable": ["osp-brave", "osp-jina"]` runs just those two.
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.
121
121
 
122
- If you specify both, **`disable` takes priority on any overlap**:
122
+ If both are specified, **`disable` takes precedence on any overlap**:
123
123
 
124
124
  ```jsonc
125
125
  {
126
126
  "options": {
127
127
  "enable": ["osp-brave", "osp-jina", "osp-serper"],
128
128
  "disable": ["osp-serper", "osp-youcom"]
129
- // osp-serper stays excluded: disable overrides enable.
130
- // osp-youcom wasn't in enable, so this is safely ignored.
129
+ // osp-serper is excluded: disable overrides enable.
130
+ // osp-youcom was not in enable, so it is safely ignored.
131
131
  }
132
132
  }
133
133
  ```
134
134
 
135
- The active pool is simply `enable - disable`. Overlapping IDs are dropped, and invalid or unrecognized IDs trigger a warning in the logs rather than failing silently.
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.
136
136
 
137
137
  ## Upgrade
138
138
 
139
139
  ```sh
140
- # installed from npm
140
+ # Installed from npm
141
141
  opencode plugin update opencode-search-plugin
142
- # installed from GitHub
142
+ # Installed from GitHub
143
143
  opencode plugin update github:guotree/opencode-search-plugin
144
- # everything outdated
144
+ # Update all outdated plugins
145
145
  opencode plugin update
146
146
  ```
147
147
 
148
- 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 (`@<version>` on npm, `#v<version>` 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.
149
153
 
150
154
  ## Uninstall
151
155
 
152
156
  ```sh
153
- # installed from npm
157
+ # Installed from npm
154
158
  opencode plugin remove opencode-search-plugin
155
- # installed from GitHub
159
+ # Installed from GitHub
156
160
  opencode plugin remove github:guotree/opencode-search-plugin
157
161
  ```
158
162
 
159
- 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.
160
164
 
161
165
  ## Keys
162
166
 
163
- 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:
164
168
 
165
- - **`/connect`** inside OpenCode: stored per provider, rotated without a restart, survives environment changes.
166
- - **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.
167
171
 
168
- 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.
169
173
 
170
- 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.
171
175
 
172
- 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.
173
177
 
174
- With keys in place, `websearch.provider` decides who answers: `"random"` spreads queries across every bound provider (set it once as in [Skip the provider prompt](#skip-the-provider-prompt)); a specific ID such as `"osp-brave"` pins one, and `false` turns 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.
175
179
 
176
180
  ## Providers
177
181
 
178
- Each ID below carries the `osp-` prefix to avoid colliding 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`.
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.
179
183
 
180
- In OpenCode's `/connect` settings, each provider is labeled with an `(osp)` suffix (e.g. `DeepSeek (osp)`) so you can tell them apart from built-in integrations. The search pool itself displays the clean provider name without the suffix.
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.
181
185
 
182
186
  | Provider | ID | Key (env / `/connect`) | Anonymous | Notes |
183
187
  | --- | --- | --- | --- | --- |
184
- | AnySearch | `osp-anysearch` | `OSP_ANYSEARCH_API_KEY` | yes | Free tier per IP; intent router, `cn`/`intl` zones |
185
- | 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 |
186
190
  | Brave Search | `osp-brave` | `OSP_BRAVE_API_KEY` | no | Independent index, 20 results max |
187
- | DeepSeek | `osp-deepseek` | `OSP_DEEPSEEK_API_KEY` | no | In-house index; $0.75 per 1,000 successful requests; 5 QPS cap |
188
- | Jina | `osp-jina` | `OSP_JINA_API_KEY` | no | Page content is opt-in because it is token-billed |
189
- | Keenable | `osp-keenable` | `OSP_KEENABLE_API_KEY` | yes | 100,000 requests a month with a key; keyless tier is per IP |
190
- | 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` |
191
195
  | Metaso | `osp-metaso` | `OSP_METASO_API_KEY` | no | Billed per search, not per result |
192
- | 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 |
193
- | Perplexity | `osp-perplexity` | `OSP_PERPLEXITY_API_KEY` | no | Search API returns bare results with no generated answer to strip; billed per request, $5.00 per 1,000 and $1.00 with `fast` |
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`) |
194
198
  | Querit | `osp-querit` | `OSP_QUERIT_API_KEY` | no | Falls back to sentence matches when a snippet is missing |
195
- | 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 |
196
200
  | Serper | `osp-serper` | `OSP_SERPER_API_KEY` | no | Google via Serper; billed per query in credits |
197
- | 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 |
198
203
 
199
204
  ## Configuration
200
205
 
201
- Configuration options are read as environment variables on startup. If you change a setting, restart OpenCode for it to take effect. The defaults reflect what each API recommends.
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.
202
207
 
203
208
  ### Shared by every provider
204
209
 
205
- | Variable | Default | Meaning |
210
+ | Variable | Default | Description |
206
211
  | --- | --- | --- |
207
- | `<PROVIDER>_CACHE_TTL` | `600000` | Milliseconds to reuse a response for the same normalized query. `0` disables caching. |
208
- | `<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) |
209
214
 
210
215
  ### AnySearch
211
216
 
212
- | Variable | Default | Meaning |
217
+ | Variable | Default | Description |
213
218
  | --- | --- | --- |
214
- | `ANYSEARCH_MAX_RESULTS` | `10` | Results per search, clamped to the API's 1–10 |
215
- | `ANYSEARCH_TAG` | — | Pin the intent router to a sub-domain, e.g. `code.doc` |
216
- | `ANYSEARCH_ZONE` | — | `cn` or `intl` |
217
- | `ANYSEARCH_LANGUAGE` | — | Preferred result language, e.g. `zh-CN` |
218
- | `ANYSEARCH_FORMAT` | `markdown` | `markdown` or `json` for the content field |
219
- | `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 |
220
225
 
221
226
  ### Bocha
222
227
 
223
- | Variable | Default | Meaning |
228
+ | Variable | Default | Description |
224
229
  | --- | --- | --- |
225
230
  | `BOCHA_COUNT` | `10` | Results per search, clamped to 1–50 (Bocha's maximum) |
226
- | `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 |
227
- | `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) |
228
233
 
229
234
  ### Brave Search
230
235
 
231
- | Variable | Default | Meaning |
236
+ | Variable | Default | Description |
232
237
  | --- | --- | --- |
233
238
  | `BRAVE_NUM` | `8` | Results per search, clamped to 1–20 (Brave's maximum) |
234
239
  | `BRAVE_COUNTRY` | — | Two-letter country code, or `ALL` |
235
- | `BRAVE_LANG` | — | Result language, e.g. `en`, `zh-hans`, `pt-br` |
236
- | `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` |
237
242
 
238
243
  ### DeepSeek
239
244
 
240
- | Variable | Default | Meaning |
245
+ | Variable | Default | Description |
241
246
  | --- | --- | --- |
242
- | `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 |
243
248
 
244
249
  ### Jina
245
250
 
246
- | Variable | Default | Meaning |
251
+ | Variable | Default | Description |
247
252
  | --- | --- | --- |
248
- | `JINA_NUM` | `0` | Results per search, 1–100. `0` omits the parameter, which keeps Jina's faster and wider result types |
249
- | `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 |
250
255
  | `JINA_GL` / `JINA_HL` | — | Country / language code |
251
- | `JINA_LOCATION` | — | City-level origin to simulate, e.g. `Berlin` |
252
- | `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 |
253
258
 
254
259
  ### Keenable
255
260
 
256
- | Variable | Default | Meaning |
261
+ | Variable | Default | Description |
257
262
  | --- | --- | --- |
258
263
  | `KEENABLE_MAX_RESULTS` | `10` | Results per search, clamped to 1–50 |
259
- | `KEENABLE_MODE` | — | `pro` (deeper retrieval) or `realtime` (fastest); unset leaves Keenable's own default |
260
- | `KEENABLE_TITLE` | `opencode-search-plugin` | App name sent as `X-Keenable-Title`, which the keyless endpoint requires to attribute the call |
261
- | `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 |
262
267
 
263
268
  ### Linkup
264
269
 
265
- | Variable | Default | Meaning |
270
+ | Variable | Default | Description |
266
271
  | --- | --- | --- |
267
- | `LINKUP_DEPTH` | `fast` | `flash`, `fast`, `standard`, or `deep` |
272
+ | `LINKUP_DEPTH` | `fast` | Search depth: `flash`, `fast`, `standard`, or `deep` |
268
273
  | `LINKUP_MAX_RESULTS` | `10` | Results per search, clamped to 1–100 |
269
274
 
270
275
  ### Metaso
271
276
 
272
- | Variable | Default | Meaning |
277
+ | Variable | Default | Description |
273
278
  | --- | --- | --- |
274
279
  | `METASO_SEARCH_SIZE` | `20` | Results per search, clamped to 1–100 |
275
- | `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 |
276
281
 
277
282
  ### Ollama
278
283
 
279
- | Variable | Default | Meaning |
284
+ | Variable | Default | Description |
280
285
  | --- | --- | --- |
281
- | `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 |
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 |
282
287
 
283
288
  ### Perplexity
284
289
 
285
- | Variable | Default | Meaning |
290
+ | Variable | Default | Description |
286
291
  | --- | --- | --- |
287
- | `PERPLEXITY_MAX_RESULTS` | `10` | Results per search, clamped to 1–20. Web search tops out at 20; only `search_type=people` accepts up to 50 |
288
- | `PERPLEXITY_SEARCH_TYPE` | — | `web` (the API's own default, $5.00 per 1,000 requests), `fast` ($1.00, same response shape, lower latency) or `people`; unset omits the field |
289
- | `PERPLEXITY_RECENCY` | — | `hour`, `day`, `week`, `month`, or `year` |
290
- | `PERPLEXITY_COUNTRY` | — | Two-letter ISO 3166-1 country code, e.g. `US` |
291
- | `PERPLEXITY_LANGUAGE` | — | ISO 639-1 codes, comma-separated, e.g. `en,zh`. At most 10 are sent: the spec allows 20 where the quickstart warns of 10, and this honours the stricter of the two |
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) |
292
297
 
293
- There is deliberately no domain or source knob: the API has no news, Reddit or academic switch, and its docs reach those verticals through `search_domain_filter` allowlists, which is a per-query decision rather than an installation-wide one.
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.
294
299
 
295
300
  ### Querit
296
301
 
297
- | Variable | Default | Meaning |
302
+ | Variable | Default | Description |
298
303
  | --- | --- | --- |
299
304
  | `QUERIT_COUNT` | `8` | Results per search |
300
305
 
301
306
  ### SerpApi
302
307
 
303
- | Variable | Default | Meaning |
308
+ | Variable | Default | Description |
304
309
  | --- | --- | --- |
305
310
  | `SERPAPI_GL` / `SERPAPI_HL` | — | Country / language code |
306
- | `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) |
307
312
 
308
- 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.
309
314
 
310
315
  ### Serper
311
316
 
312
- | Variable | Default | Meaning |
317
+ | Variable | Default | Description |
313
318
  | --- | --- | --- |
314
319
  | `SERPER_NUM` | `10` | Results per search, clamped to 1–100 |
315
320
  | `SERPER_GL` / `SERPER_HL` | — | Country / language code |
@@ -317,48 +322,62 @@ There is deliberately no result-count knob: Google withdrew the `num` parameter,
317
322
 
318
323
  ### You.com
319
324
 
320
- | Variable | Default | Meaning |
325
+ | Variable | Default | Description |
321
326
  | --- | --- | --- |
322
327
  | `YOUCOM_COUNT` | `10` | Results per section (web and news each), clamped to 1–100 |
323
- | `YOUCOM_EXTRACTION` | `highlights` | `highlights` returns answering passages, `none` returns bare snippets, `full_page` crawls every hit and bills per live-fetched page |
324
- | `YOUCOM_LANGUAGE` / `YOUCOM_COUNTRY` | — | e.g. `EN` / `US` |
325
- | `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.
326
343
 
327
344
  ## How a query flows
328
345
 
329
- 1. OpenCode requests a search from the active provider.
330
- 2. The normalized query (trimmed and whitespace-collapsed) is checked against an in-memory cache. Cache hits return immediately without making an API request.
331
- 3. On a cache miss, the API key is resolved fresh, so rotated keys in `/connect` take effect right away.
332
- 4. The request goes out with an abort deadline (`<PROVIDER>_TIMEOUT`, default 25s) to prevent stalled connections from hanging the session.
333
- 5. The response is schema-validated, mapped to `{ url, title, content, time }`, and cached for repeat queries.
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.
334
351
 
335
- Relative dates (`3 hours ago`, `3小时前`), absolute dates (`Sep 30, 2026`, `31 Mar 2026`), and ISO 8601 timestamps are parsed automatically. Incomplete strings that might mislead lenient parsers (like `Nov 1`) are ignored instead of guessing an incorrect year.
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.
336
353
 
337
354
  ## Troubleshooting
338
355
 
339
356
  ```sh
340
- # Is the plugin loaded, and did it fail?
357
+ # Check if the plugin is loaded and view its status
341
358
  opencode plugin list
342
359
 
343
- # Which providers are actually in the pool?
344
- # A provider missing here has no key, or `enable`/`disable` kept it out.
345
- # 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
346
363
  opencode api get "/api/websearch/provider"
347
364
  ```
348
365
 
349
- Also worth checking:
366
+ Common issues to check:
350
367
 
351
- - **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.
352
- - **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.
353
- - **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.
354
- - **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.
355
- - **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.
356
- - **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.
357
- - **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.
358
- - **Everything is slow or burning credits**: leave `*_CACHE_TTL` at its default. Repeated queries within the window are served from memory.
359
- - **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`.
360
379
 
361
- 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.
362
381
 
363
382
  ## Development
364
383
 
@@ -367,12 +386,12 @@ npm install
367
386
  npm run check # typecheck + tests
368
387
  ```
369
388
 
370
- - `npm run typecheck`: strict TypeScript across every plugin and test.
371
- - `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.
372
391
 
373
- `@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.
374
393
 
375
- 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).
376
395
 
377
396
  ## License
378
397