deepseek-web-retrieval 0.6.0 → 0.8.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +113 -53
  3. package/dist/cli.js +2427 -1813
  4. package/docs/adr/0002-shared-config-base-layer.md +43 -0
  5. package/docs/agents/claude-code.md +273 -0
  6. package/docs/agents/codex.md +50 -27
  7. package/docs/agents/opencode-v2.md +59 -33
  8. package/docs/agents/pi.md +50 -27
  9. package/docs/configuration.md +109 -0
  10. package/docs/design/config-layering-design.md +180 -0
  11. package/docs/design/installer-design.md +24 -13
  12. package/docs/hosts.jsonc +5 -1
  13. package/docs/integration.md +15 -9
  14. package/docs/research/control-matrix.md +49 -11
  15. package/docs/research/mcp-compatibility.md +6 -3
  16. package/docs/research/ori-question-sourcing.md +336 -0
  17. package/docs/research/result-quality-passthrough.md +159 -0
  18. package/docs/research/search-api-defaults.md +18 -2
  19. package/docs/research/search-inline-content.md +244 -0
  20. package/examples/opencode.global.jsonc +3 -2
  21. package/package.json +2 -1
  22. package/src/core/auth.ts +3 -0
  23. package/src/core/client.ts +26 -4
  24. package/src/core/config-file.ts +168 -0
  25. package/src/core/config-summary.ts +82 -0
  26. package/src/core/constants.ts +15 -1
  27. package/src/core/descriptions.ts +6 -2
  28. package/src/core/env.ts +102 -32
  29. package/src/core/format.ts +19 -1
  30. package/src/core/options.ts +22 -2
  31. package/src/core/search.ts +6 -0
  32. package/src/core/types.ts +11 -0
  33. package/src/installer/cli.ts +50 -16
  34. package/src/installer/config-command.ts +116 -0
  35. package/src/installer/hosts/claude-code.ts +325 -0
  36. package/src/installer/hosts/codex.ts +9 -4
  37. package/src/installer/hosts/index.ts +3 -0
  38. package/src/installer/hosts/opencode.ts +9 -5
  39. package/src/installer/hosts/pi.ts +34 -9
  40. package/src/installer/ownership.ts +54 -1
  41. package/src/installer/paths.ts +7 -1
  42. package/src/installer/state.ts +73 -20
  43. package/src/installer/types.ts +11 -1
  44. package/src/installer/uninstall.ts +4 -1
  45. package/src/mcp/auth.ts +2 -3
  46. package/src/mcp/cli.ts +4 -3
  47. package/src/mcp/serve.ts +10 -1
  48. package/src/mcp/server.ts +7 -2
  49. package/src/opencode/plugin.ts +22 -2
  50. package/src/pi/extension.ts +78 -9
@@ -0,0 +1,159 @@
1
+ # Passing result quality fields to the model
2
+
3
+ This note answers one question: through which channel can DeepSeek's per-result quality fields —
4
+ `rerank_score` (0–3, "≥1.5 优质"), `authority` (0–4, -1 unknown, "≥3 极权威"), `site_name`
5
+ (string|null) — reach the **model** (not the human UI) on each host at its locked version? The
6
+ repo filters on the first two in `src/core/search.ts` (thresholds default to off); at the
7
+ research baseline it dropped all three before returning — the adapters now pass them through
8
+ as a text marker plus a structured mirror (item 1 below is implemented as
9
+ `formatQualityMarker`).
10
+ Related: [ori-question-sourcing.md](ori-question-sourcing.md) (input side),
11
+ [search-inline-content.md](search-inline-content.md) (content volume).
12
+
13
+ **Research baseline:** 2026-10-23, using the same version locks as
14
+ [hosts.jsonc](../hosts.jsonc) (Cline 0.0.13, Codex 0.163.0, OpenCode 2.0.32, Pi 1.3.0,
15
+ Claude Code 2.1.303, Cursor 2026.10.08, Devin 3000.11.92, GitHub Copilot 1.0.98). Claims are
16
+ cited to primary sources: the MCP specification, the official TypeScript SDK as installed in
17
+ this repo (`@modelcontextprotocol/server` 2.3.1 — the shipped artifact), host source at the
18
+ exact tags or as installed in `node_modules` (the published npm packages are themselves primary
19
+ sources), and official docs/changelogs. Prior findings in
20
+ [mcp-compatibility.md](mcp-compatibility.md) and [control-matrix.md](control-matrix.md) are
21
+ cited rather than re-derived; this note re-verified the load-bearing ones and extends them.
22
+
23
+ ## What the specification says
24
+
25
+ MCP [`2025-06-18` tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
26
+ introduced structured output:
27
+
28
+ - Unstructured content rides in `content` (text/image/audio/resource blocks); structured
29
+ content is "a JSON object in the `structuredContent` field."
30
+ - Backwards compatibility is a server-side SHOULD, verbatim: *"For backwards compatibility, a
31
+ tool that returns structured content **SHOULD** also return the serialized JSON in a
32
+ TextContent block."*
33
+ - With a declared `outputSchema`: *"Servers **MUST** provide structured results that conform to
34
+ this schema. Clients **SHOULD** validate structured results against this schema."*
35
+ - Content blocks carry optional `annotations.audience` (`"user"` / `"assistant"`) — a spec-level
36
+ way to mark a block as model-visible or human-visible. No host in the matrix is documented to
37
+ honor it; treated as unpinned below.
38
+
39
+ The `2024-11-05` revision (Cline's floor) has neither `structuredContent` nor `outputSchema`.
40
+
41
+ ## What the official TypeScript SDK enforces (v2.3.1, as shipped in this repo)
42
+
43
+ The server-side validator (`@modelcontextprotocol/server` 2.3.1,
44
+ `dist/mcp-*.mjs`, `validateToolOutput`) makes the outputSchema obligation concrete:
45
+
46
+ - A tool with an `outputSchema` whose result has no `structuredContent` is **rejected**:
47
+ `Output validation error: Tool <name> has an output schema but no structured content was
48
+ provided` (JSON-RPC `InvalidParams`). Text-only results are not allowed for schema'd tools.
49
+ - `structuredContent` is then validated against the schema; a mismatch is rejected the same way.
50
+ - The v2 codec's SEP-2106 §4.3 auto-append (`appendTextFallbackForNonObject`) adds a
51
+ `JSON.stringify(structuredContent)` TextContent block **only when `structuredContent` is a
52
+ non-object** (primitive/array/null) and no text block exists. Object-shaped
53
+ `structuredContent` gets **no** automatic text mirror — the server must mirror it itself to
54
+ satisfy the spec's SHOULD.
55
+
56
+ Consequence for this repo: the shipped server declares no `outputSchema` and emits both a text
57
+ summary and (except for Codex, below) a mirrored `structuredContent` — that combination remains
58
+ the spec- and SDK-safe shape. Declaring an `outputSchema` would obligate every successful call
59
+ to return schema-conforming structured content.
60
+
61
+ ## Per host: what actually reaches the model
62
+
63
+ | Host (lock) | Text `content` reaches model | `structuredContent` reaches model | Output caps at the lock |
64
+ | --- | --- | --- | --- |
65
+ | Codex 0.163.0 | Replaced when `structuredContent` present | Yes — content is **replaced** by serialized JSON | Per-tool `output_token_limit` + "standard 20% serialization allowance" ([mcp_types.rs](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/config/src/mcp_types.rs)); default value not pinned there |
66
+ | Pi 1.3.0 (MCP path) | Yes (content-first) | Only when no content blocks | 20 KB text middle-truncate, full text saved to file; 60 s per-request ([control-matrix](control-matrix.md#pi)) |
67
+ | Pi 1.3.0 (extension path) | Yes — **the only model channel** | **No** — event-layer only (below) | Extension tools bypass the MCP 20 KB truncation ([control-matrix](control-matrix.md#pi)) |
68
+ | OpenCode 2.0.32 (plugin path) | Yes (content wins) | **No** — serialized `output` is a fallback only (below) | 2000 lines / 50 KiB tool-output truncation, full output retained to file for 7 days |
69
+ | Claude Code 2.1.303 | Yes | Spec-level yes (2026-07-28 + TS SDK 2.0 runtime); client-side rendering not pinned by a primary source | 25k tokens default (`MAX_MCP_OUTPUT_TOKENS`), oversized saved to file ([control-matrix](control-matrix.md#claude-code)) |
70
+ | Cline 0.0.13 | Yes | **No** — `2024-11-05` has no `structuredContent` | None documented |
71
+ | Devin 3000.11.92 / Copilot 1.0.98 | Yes | Spec-level yes (both negotiate 2026-07-28); rendering not pinned | Not pinned |
72
+ | Cursor 2026.10.08 | Yes | Revision unpinnable (docs show ≥2025-06-18 features, so the field exists); rendering not pinned | Not pinned |
73
+
74
+ Codex's replacement behavior and Cline's text-only floor were established at these locks in
75
+ [mcp-compatibility.md](mcp-compatibility.md#what-each-host-supports) and re-checked; the two
76
+ native-adapter findings below are new.
77
+
78
+ ### Pi 1.3.0: the extension `structuredContent` never reaches the model
79
+
80
+ From the installed `@earendil-works/pi-coding-agent` 1.3.0 (the shipped artifact):
81
+
82
+ - An extension tool's `execute` returns `{ content, structuredContent?, details? }`. The
83
+ `structuredContent` field exists only on the extension **event** layer, documented as
84
+ *"Machine-readable result for tools that declare an `outputSchema`"*
85
+ (`dist/core/extensions/types.d.ts`, `ToolResultEventBase`) — consumers are other extensions
86
+ (hooks, nested tool calls), not the model.
87
+ - The model-facing message type (`@earendil-works/pi-ai` `ToolResultMessage`,
88
+ `dist/types.d.ts`) carries `content` blocks, `isError`, ids — no `structuredContent`, and
89
+ `details` is session-record only. The Anthropic serializer sends exactly
90
+ `tool_use_id` + content blocks + `is_error`
91
+ (`dist/api/anthropic-messages.js`, `convertToolResult`).
92
+ - So this repo's Pi adapter already does the right thing by putting the full result in the text
93
+ block; its `structuredContent` payload is event-layer data for machine consumers.
94
+
95
+ ### OpenCode 2.0.32: text `content` wins; typed `output` is a JSON-stringified fallback; `metadata` is not model-facing
96
+
97
+ From the official repo at tag `v2.0.32` (github.com/anomalyco/opencode, where `@opencode/cli`
98
+ 2.0.32's npm manifest points) and the installed `@opencode/schema`/`@opencode/plugin`:
99
+
100
+ - `Tool.Result` is `{ output?, content?, metadata? }` — `output` typed by an optional declared
101
+ output schema, `content` a string or text/file blocks, `metadata` a free record
102
+ (`@opencode/schema/dist/tool.d.ts`).
103
+ - The model channel is decided by
104
+ [`normalizeContent(content, output)`](https://github.com/anomalyco/opencode/blob/v2.0.32/packages/core/src/tool/runtime.ts):
105
+ a string or non-empty `content` wins; only when `content` is absent/empty is `output`
106
+ serialized via `JSON.stringify` into a text block. `metadata` never enters this path (it
107
+ feeds `tool.execute.after` hooks and UI).
108
+ - Every tool result then passes through `ToolOutput.truncate`:
109
+ **`MAX_LINES = 2000`, `MAX_BYTES = 50 KiB`** (full output retained to a `tool-output/`
110
+ file for 7 days) — [`packages/core/src/tool-output.ts`](https://github.com/anomalyco/opencode/blob/v2.0.32/packages/core/src/tool-output.ts).
111
+ These caps were not previously recorded in [control-matrix.md](control-matrix.md).
112
+
113
+ ## What this means for this repo
114
+
115
+ 1. **Text is the only channel verified to reach the model on every host.** Codex is the sole
116
+ host where `structuredContent` demonstrably reaches the model (as a *replacement*); on the
117
+ two native adapters it provably does not (Pi: event-layer only; OpenCode: fallback only).
118
+ Quality fields the model should reason about therefore belong **in the text** — e.g. a
119
+ compact per-result marker like `[rerank 2.1 · authority 3]` (plus `site_name` where present).
120
+ At 10 results that is a few hundred characters, negligible against every cap above.
121
+ 2. **Mirroring in `structuredContent` stays worth it** for machine consumers (Pi extension
122
+ events, future hosts, tests) and costs nothing: the shipped dual-emission — text plus
123
+ structured, with the mirror omitted for the detected `codex-mcp-client` identity — already
124
+ matches the spec's SHOULD and Codex's replacement. Adding the new fields to both shapes
125
+ changes no host behavior.
126
+ 3. **Keep `outputSchema` undeclared** on the MCP server unless the team is prepared to guarantee
127
+ schema-conforming `structuredContent` on every successful call (the SDK rejects otherwise).
128
+ The Pi adapter's extension `outputSchema` is safe as-is — Pi re-declares but does not
129
+ validate it, and its `structuredContent` is event data.
130
+ 4. **New cap facts for the design margin:** OpenCode plugin results truncate at 2000 lines /
131
+ 50 KiB (new), Pi extension tools bypass the MCP 20 KB truncation (already known), Claude
132
+ Code 25k tokens, Codex per-tool `output_token_limit` (default unpublished). None of these
133
+ bind at search-result volumes; they matter if [inline content](search-inline-content.md) is
134
+ ever enabled.
135
+
136
+ ## Sources
137
+
138
+ - MCP spec: [2025-06-18 tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
139
+ (structured content, outputSchema MUST/SHOULD, audience annotations); revision table in
140
+ [mcp-compatibility.md](mcp-compatibility.md#protocol-revisions).
141
+ - Official TypeScript SDK, **as installed** (`@modelcontextprotocol/server` 2.3.1,
142
+ `@modelcontextprotocol/client` 2.3.1): `dist/mcp-*.mjs` `validateToolOutput` /
143
+ `validateToolInput`; `dist/src-*.mjs` `appendTextFallbackForNonObject` /
144
+ `projectCallToolResult` (SEP-2106 §4.3).
145
+ - Pi, **as installed** (`@earendil-works/pi-coding-agent` 1.3.0, `@earendil-works/pi-ai`):
146
+ `dist/core/extensions/types.d.ts` (`ToolResultEventBase.structuredContent` doc,
147
+ `ToolDefinition.outputSchema`), `pi-ai/dist/types.d.ts` (`ToolResultMessage`),
148
+ `pi-ai/dist/api/anthropic-messages.js` (`convertToolResult`, `convertMessages`).
149
+ - OpenCode at the lock: [`v2.0.32` tool/runtime.ts (`normalizeContent`)](https://github.com/anomalyco/opencode/blob/v2.0.32/packages/core/src/tool/runtime.ts),
150
+ [`v2.0.32` tool-output.ts (MAX_LINES/MAX_BYTES)](https://github.com/anomalyco/opencode/blob/v2.0.32/packages/core/src/tool-output.ts),
151
+ [`v2.0.32` core/src/tool.ts (execute.after result shape)](https://github.com/anomalyco/opencode/blob/v2.0.32/packages/core/src/tool.ts),
152
+ installed `@opencode/schema/dist/tool.d.ts` and `@opencode/plugin/dist/promise/tool.d.ts`.
153
+ - Codex: [`mcp_types.rs` at rust-v0.163.0 (`output_token_limit`, 20% serialization allowance)](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/config/src/mcp_types.rs);
154
+ replacement behavior per [mcp-compatibility.md](mcp-compatibility.md#what-each-host-supports).
155
+ - Host caps and protocol facts previously verified at these locks:
156
+ [control-matrix.md](control-matrix.md#per-host-reference) (Pi 20 KB / 60 s, Claude Code
157
+ `MAX_MCP_OUTPUT_TOKENS` 25k, Cline text-only floor),
158
+ [mcp-compatibility.md](mcp-compatibility.md) (per-host negotiated revisions; Cursor
159
+ unpinnable).
@@ -255,9 +255,25 @@ quality filtering**. What they do publish, verbatim
255
255
  - `rerank_score`: "排序分数,0~3 分,综合考虑相关性、权威性、时效性等维度,**大于等于 1.5 分为优质结果**。"
256
256
  - `authority`: "网页权威性,0~4(**-1 表示未知**),**大于等于 3 为极权威结果**。"
257
257
  - Request defaults: `count` "否 / 默认值 10 / 返回条数,1~50"; `need_content` "否 / false";
258
- `localization` "否 / 空"; `ori_question` optional.
258
+ `ori_question` optional ("触发搜索的用户原始问题,1~128 个字符").
259
+ - `localization` (checked 2026-10-23): documented as "用于优先返回本地化搜索结果的 IP
260
+ 地址" — an IP address used to prefer geo-localized results, optional, default empty. The
261
+ docs pin nothing beyond "IP 地址"; this repo accepts exactly what `net.isIP` validates
262
+ (IPv4 or IPv6) and drops anything else at option resolution.
263
+ - Query operators (checked 2026-10-23): the `query` "会原样传递给搜索服务" and supports
264
+ `site:` / `-site:` / `lang:` (e.g. `lang:zh,en`) plus the time filters `after:` ("将结果限制在指定日期之后发布的网页")
265
+ and `before:` ("将结果限制在指定日期之前发布的网页") — each taking a year or a date, with the
266
+ doc examples "DeepSeek after:2025 或 DeepSeek after:2025-01-01" and
267
+ "DeepSeek before:2026 或 DeepSeek before:2026-06-01".
259
268
  - Response sizing: `snippet` "短摘要 … 通常为 100~200 字符"; `summary` "长摘要 … 通常为 600~800 字符".
260
269
 
270
+ Two follow-up notes examine the request parameters this section lists only as defaults:
271
+ [search-inline-content.md](search-inline-content.md) covers `need_content: true` — what
272
+ provably returns, what it costs, how large it can get, and where that collides with
273
+ per-host output caps — and [ori-question-sourcing.md](ori-question-sourcing.md) covers
274
+ `ori_question` — the per-host channels that could supply a value (the MCP and Pi adapters
275
+ now send it; OpenCode's websearch provider path has no input channel at its lock).
276
+
261
277
  ## What this means for this repo's defaults
262
278
 
263
279
  Fact-by-fact alignment of the repo's client-side defaults
@@ -268,7 +284,7 @@ here; the table only lines up what the industry ships.
268
284
  | --- | --- |
269
285
  | API `count=10`, client `maxResults=10` (post-dedup cap; `5` before 2026-10-21) | API-level count defaults: Tavily 10, Exa 10, Perplexity Search API 10, Firecrawl 10, You.com 10 (per section), Brave 20. Post-filter caps are uncommon; a fixed 5 exists at Jina (`s.jina.ai` top 5, not configurable) and Perplexity's `image_search` tool (default 5). Linkup's schema example is 5 but no default is published. OpenAI and Anthropic expose no count at all |
270
286
  | `minRerank=0`, `minAuthority=0`, `allowUnknownAuthority=true` — no client-side filtering (previously `1.5`/`2`/`false`) | **No surveyed product filters search results client-side by relevance score or authority.** The only published thresholds for DeepSeek's own fields are the docs' quality labels (≥1.5 "优质", ≥3 "极权威") — labels, not filters. The closest shipped analogues are domain allow/block lists (Tavily, Exa, Perplexity, You.com, Anthropic, Brave) and You.com's `safesearch: moderate` default, which is content moderation, not authority |
271
- | `maxRetries=0` | Official model-API SDKs: OpenAI 2, Anthropic 2 (both languages). Search-API SDKs: firecrawl-js 3 (502 only), tavily-python 0, Linkup SDK 0 |
287
+ | `maxRetries=2` — changed from `0` on 2026-10-23: this package runs as an agent tool, where a transient failure left un-retried falls to the model re-issuing the whole billed tool call, the failure profile of the model-API SDKs in this row (2, both languages) rather than of the split search-API SDKs; the default followed the model-SDK column | Official model-API SDKs: OpenAI 2, Anthropic 2 (both languages). Search-API SDKs: firecrawl-js 3 (502 only), tavily-python 0, Linkup SDK 0 |
272
288
  | `timeout=30s` (search and fetch, 1–120 s range, covers body read) | Search/scrape API timeouts: Firecrawl 60 s (scrape max 300 s), Tavily SDK search 60 s (cap 120 s) / extract 30 s, You.com `crawl_timeout` 10 s, Linkup SDK no timeout, Jina `x-timeout` unset by default (max 180 s). Model-API SDKs: OpenAI and Anthropic 10 minutes |
273
289
  | Fetch truncation 16,000 chars | Exa: page text **off** by default (`contents.text: false`), opt-in `maxCharacters` with no published default; DeepSeek itself: `need_content: false` by default; Anthropic `web_fetch`: token-based `max_content_tokens`, no published default (example 100000), no char/MiB figure in current docs; You.com highlights budget 8192 tokens; Firecrawl and Jina publish no truncation default |
274
290
  | Summary truncation 1,200 chars | DeepSeek's own field doc says `summary` is "通常为 600~800 字符"; no surveyed vendor ships a client-side summary cap (they return snippets/highlights sized server-side: Exa verbosity `compact`, You.com highlights budget) |
@@ -0,0 +1,244 @@
1
+ # Search with inline content: `need_content` facts
2
+
3
+ This note answers one question: if DeepSeek's Search API were called with
4
+ `need_content: true` (today the repo hardcodes `false`), what would the response provably
5
+ contain, what would it cost, how large could it get, and where does that collide with
6
+ per-host output caps? It extends
7
+ [search-api-defaults.md](search-api-defaults.md) — which surveyed factory defaults — with the
8
+ one question that survey did not ask: which vendors ship search-with-inline-content as the
9
+ default architecture versus opt-in, and with what per-result content budget. It records facts
10
+ only; the closing section lines the facts up against the current two-tool design (search
11
+ summary-only plus a separate fetch tool) without recommending for or against it.
12
+
13
+ **Research baseline: 2026-10-23.** Claims are cited to primary sources only: official
14
+ DeepSeek documentation (api-docs.deepseek.com, English and 中文 pages, plus the official FAQ
15
+ bundle), other vendors' official documentation and SDK/API references, and OpenAI Codex
16
+ source at the repo's locked tag. Secondhand blogs are excluded. Where no primary source pins
17
+ a fact — most importantly the size of `content` — that is stated as such: an undocumented
18
+ size is itself a decision-relevant fact.
19
+
20
+ ## What `need_content=true` provably returns
21
+
22
+ From the official Search & Fetch guide, the parameter and its response field, verbatim:
23
+
24
+ | Item | 中文 | English |
25
+ | --- | --- | --- |
26
+ | `need_content` (request) | “是否在 content 中返回网页正文,为 false 时 content 恒为空字符串。” | "Whether to return page content in content, which is always an empty string when false." |
27
+ | `content` (search response) | “网页正文,need_content 为 false 或正文缺失时为空字符串。” | "The page content, which is empty when need_content is false or when the content is unavailable." |
28
+
29
+ ([指南](https://api-docs.deepseek.com/zh-cn/guides/search) ·
30
+ [guide](https://api-docs.deepseek.com/guides/search), checked 2026-10-23.)
31
+
32
+ Three facts follow, all of them absences:
33
+
34
+ 1. **No size bound of any kind is documented for `content`.** The same guide gives "通常"
35
+ sizing for the other two text fields — `snippet` "通常为 100~200 字符" ("typically 100 to
36
+ 200 characters"), `summary` "通常为 600~800 字符" ("typically 600 to 800 characters") —
37
+ but has **no analogous statement** for `content`: no "通常" range, no maximum, no
38
+ truncation rule, no server-side character cap. An undocumented content size is the
39
+ single most decision-relevant fact in this note.
40
+ 2. **No format statement for search `content`.** The Fetch API's `content` field is
41
+ explicitly Markdown — "The fetched page content as Markdown text." /
42
+ “抓取后的 Markdown 正文” — but the Search API's `content` field carries no format
43
+ statement. Whether search-side `content` is the same Markdown rendering the Fetch
44
+ endpoint returns, or something else, is not documented. “正文缺失时为空字符串”
45
+ ("or when the content is unavailable") implies per-result content can be missing
46
+ independently of `need_content`, which matches the Fetch API's own empty-content failure
47
+ mode; no recovery behavior is specified for search.
48
+ 3. **No `need_content` × `count` interaction is documented.** The only sentence linking a
49
+ request parameter to result shape is about count and price (below); nothing says content
50
+ is returned for only the top N results, or that `count` behaves differently under
51
+ `need_content`.
52
+
53
+ ## Billing and rate limits
54
+
55
+ **Billing is per successful request, and `need_content` is never mentioned in connection
56
+ with price.** Verbatim from the guide and pricing page:
57
+
58
+ - “Search 和 Fetch 按请求次数计费,不按 token 或返回结果条数计费” — Search and Fetch are
59
+ billed per request, **not by token or by number of results returned**
60
+ ([指南](https://api-docs.deepseek.com/zh-cn/guides/search)).
61
+ - "count only controls how many results are returned and does not affect the per-request
62
+ price." / “count 只控制返回的结果条数,不影响单次请求的价格。” (same guide, both languages.)
63
+ - Price: Search and Fetch are each "Per 1,000 successful requests $0.75"
64
+ ([pricing](https://api-docs.deepseek.com/quick_start/pricing); the 中文 pages state
65
+ 每 1,000 次成功请求 5 元). A request is billable "only when it returns HTTP 200 and
66
+ produces a result" (Search must return non-empty `results`; Fetch must return page
67
+ content); failures, empty results, and timeouts are free.
68
+ - Neither the guide, the pricing page, nor the official FAQ
69
+ (`static.deepseek.com/faq`, whose embedded question set was inspected — it contains zero
70
+ entries about the Search/Fetch API) mentions `need_content` as a price modifier. Under
71
+ the documented model, a `need_content: true` search is one request at the Search
72
+ per-request price; there is no token-metered billing tier for Search/Fetch to move into.
73
+
74
+ One dated note on the pricing page (verbatim, EN): "when you use the DeepSeek API through
75
+ DeepSeek Harness, Claude Code, or Codex," server-side tool calls already hit the Search and
76
+ Fetch APIs; "These calls are currently free of charge;" — “这部分 Search/Fetch 接口调用目前未计费,
77
+ 并将于北京时间 2026 年 11 月 1 日 00:00 起按次收取调用费用” (per-request billing for those
78
+ server-side-tool calls starts 00:00 Beijing time on 2026-11-01 — nine days after this note's
79
+ baseline). This note concerns the repo's direct `/v1/tools/search` calls, which follow the
80
+ per-request price above, not those chat-completions-internal tool calls.
81
+
82
+ **Rate limits are per endpoint and independent; `need_content` is not mentioned.** Search
83
+ 100 QPS, Fetch 25 QPS per account; “Search 和 Fetch 分别独立限速,互不占用额度” ("traffic to
84
+ one does not consume the other's quota"); “除了账号级限制,Fetch 还针对目标域名有 QPS 限制”
85
+ (Fetch additionally has an unspecified per-domain QPS limit)
86
+ ([guide](https://api-docs.deepseek.com/guides/search)). A `need_content: true` call is a
87
+ Search-endpoint call; no page documents it counting against anything but the Search quota.
88
+
89
+ ## Official guidance on `need_content` vs the Fetch endpoint
90
+
91
+ None exists. The guide gives only the two positioning sentences — Search “按 query 搜索网页,
92
+ 返回结构化结果”, Fetch “抓取单个网页,以 Markdown 文本返回页面内容” — with no use-case,
93
+ latency, or cost trade-off statement, in either language. (Checked 2026-10-23; also absent
94
+ from the FAQ.)
95
+
96
+ ## Vendor architectures: who ships content-bearing search by default
97
+
98
+ Extending [search-api-defaults.md](search-api-defaults.md) (which established the defaults)
99
+ with the architecture reading. "Inline by default" means the caller gets page bodies in the
100
+ search response without setting any parameter.
101
+
102
+ | Vendor | Content inline by default? | Opt-in mechanism | Per-result content budget knob | Separate fetch tool? |
103
+ | --- | --- | --- | --- | --- |
104
+ | **Tavily** | Partial — default response carries per-result `content` of **relevance chunks**: `chunks_per_source` **3** chunks of ~500 chars (~1,500 chars/result) | Full body via `include_raw_content` (default **false**; accepts `markdown`/`text`/`true`) | Chunk count only (`chunks_per_source` 1–3, sizes `content`, **not** `raw_content`); no knob limits `raw_content` size in `/search` | Yes — `/extract` (up to 20 URLs; `extract_depth` basic/advanced; no per-URL cap; ~800-char chunks only when `query` is given) |
105
+ | **Exa** | No — `contents.text` defaults **false** | `contents.text: true` in the **same** `POST /search` body | **`maxCharacters`** (1–1,000,000, no published default) plus `verbosity` `compact`/`standard`/`full` | Yes — `/contents` endpoint by result ID/URL |
106
+ | **You.com** | No — “Omit `extraction` for a plain search that returns snippets only” | `extraction.full_page` (“crawls each result and returns the page content”) | **None for `full_page`** (only `crawl_timeout`); the `highlights` mode has a response-wide `total_max_tokens` budget of **8192** (1,024–8,192) | No dedicated fetch endpoint documented on `/v1/search` |
107
+ | **Perplexity** | No — Search API results are `title`/`url`/`snippet`/`date` only | Token budgets: `max_tokens` ("Maximum total webpage content tokens to return across all search results") and **`max_tokens_per_page`** ("Maximum webpage content tokens to extract from each result page"), on both the Search API and the Agent API `web_search` tool; **no default published** for either ("the default budgets apply"); on the Search API the extracted text surfaces through `snippet` | **`max_tokens_per_page`** (min 1, max 1,000,000, no default) | Yes — Agent API "Fetch URL Content" tool ("Fetch full content from known URLs") |
108
+ | **Anthropic** | Not caller-visible — `web_search` results carry `url`/`title`/`page_age` plus `encrypted_content` (“Encrypted content that you must pass back in multi-turn conversations”); the model reads content in its context, the caller cannot | `response_inclusion` (`"full"` default / `"excluded"`) only controls whether consumed search blocks are echoed in the response — it is not a content-size knob | **None** (dynamic filtering on `web_search_20260209+` has Claude write code to filter results before they reach context, but no caller-set budget) | Yes — `web_fetch` with token-based `max_content_tokens` (no published default) |
109
+ | **Firecrawl** | No — search returns `title`/`description`/`url` (+ `highlights`, default true) | `scrapeOptions` (default `{}`): "Include `scrapeOptions` with `formats: [{"type": "markdown"}] to get complete markdown content for each search result" | **None** for content size (`onlyMainContent` default true trims boilerplate, not length) | Collapsed into search via `scrapeOptions`; standalone `/scrape` endpoint also exists |
110
+ | **Jina** (`s.jina.ai`) | **Yes — the only default-content architecture surveyed**: "Reader searches the web, fetches the top 5 results, visits each URL, and applies `r.jina.ai` to it"; result count is fixed at 5, not a parameter | None needed (content is the product) | **`x-token-budget`** header, no default (parsed only when sent) — a global budget, not per-result | Same product (`r.jina.ai`) serves plain fetch |
111
+ | **DeepSeek (subject of this note)** | No — `need_content` defaults **false** | `need_content: true` | **None documented** (see above) | Yes — Fetch API, Markdown, no documented size cap |
112
+
113
+ The split, stated plainly: of the eight architectures surveyed, exactly one (Jina
114
+ `s.jina.ai`) ships content-bearing search as the default, and it pins the result count at 5.
115
+ Every other vendor makes inline content opt-in (Tavily `raw_content`, Exa `contents.text`,
116
+ You.com `extraction`, Firecrawl `scrapeOptions`, Perplexity token budgets) or keeps page
117
+ content out of the caller's hands entirely (Anthropic `encrypted_content`). Where budgets
118
+ exist they are per-result character caps (Exa `maxCharacters`) or per-result token caps
119
+ (Perplexity `max_tokens_per_page`); Tavily and You.com ship full-content modes with **no**
120
+ per-result size knob at all — the two closest analogues to DeepSeek's `need_content`.
121
+
122
+ Sources: [Tavily /search](https://docs.tavily.com/documentation/api-reference/endpoint/search) ·
123
+ [Tavily /extract](https://docs.tavily.com/documentation/api-reference/endpoint/extract) ·
124
+ [Exa /search](https://exa.ai/docs/reference/search) ·
125
+ [You.com /v1/search](https://you.com/docs/api-reference/search/v1-search) ·
126
+ [Perplexity /search](https://docs.perplexity.ai/api-reference/search-post) ·
127
+ [Perplexity Agent web_search](https://docs.perplexity.ai/docs/agent-api/tools/web-search) ·
128
+ [Anthropic web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) ·
129
+ [Anthropic web fetch tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) ·
130
+ [Firecrawl /search](https://docs.firecrawl.dev/api-reference/endpoint/search) ·
131
+ [Jina reader README](https://github.com/jina-ai/reader/blob/main/README.md) — all checked
132
+ 2026-10-23; Tavily/Exa/You.com/Jina defaults cross-checked against
133
+ [search-api-defaults.md](search-api-defaults.md).
134
+
135
+ ## Volume math against host caps
136
+
137
+ **Assumptions, labeled as such (not source-backed):** a typical article page body rendered
138
+ to Markdown is ~2,000–10,000 characters (assumption; it is consistent with this repo's own
139
+ calibration that 16,000 chars is a sensible budget for one *fetched* page, but no vendor
140
+ publishes a typical body size); English text ≈ 1 byte and CJK ≈ 3 bytes per UTF-8 char;
141
+ English ≈ 4 chars per token and CJK ≈ 1–1.5 chars per token (rough tokenizer conventions,
142
+ assumptions).
143
+
144
+ With `count=10` (the repo's default, and DeepSeek's) and `need_content: true`:
145
+
146
+ - **Response size, order of magnitude:** 10 × 2k–10k chars of body ≈ **20k–100k chars**,
147
+ plus ~1k chars of metadata per result (title/url/snippet/summary/scores) → roughly
148
+ **30k–110k chars**, i.e. **~30–110 KB** for English pages and up to **~330 KB** if pages
149
+ are CJK-heavy. A single malformed huge page (a pasted log, a link farm) can exceed this
150
+ unboundedly — there is no documented server-side cap (the key fact above).
151
+ - **Pi, MCP path: collides at the low end of the assumption.** Text results over **20 KB**
152
+ are middle-truncated (full text saved to a file) and the per-request timeout is **60 s**
153
+ ([control-matrix.md](control-matrix.md)). Even 10 × 2k-char bodies plus metadata (~30 KB)
154
+ exceeds 20 KB. Staying under 20 KB at count=10 requires ≲**1.8k chars of content per
155
+ result** after metadata. Caveat from the same note: the *shipped* Pi adapter is the native
156
+ extension, whose results bypass the MCP layer — "neither the 20 KB truncation nor the 60 s
157
+ request timeout applies"; the cap binds the MCP fallback path only.
158
+ - **Claude Code: collides at the high end.** Tool output is capped at **25k tokens** by
159
+ default (`MAX_MCP_OUTPUT_TOKENS`), oversized text saved to a file
160
+ ([control-matrix.md](control-matrix.md)). 30k–110k English chars ≈ 8k–28k tokens —
161
+ brushing or overflowing the cap at the top of the assumed range; CJK at 1–1.5 chars/token
162
+ ≈ 20k–110k tokens — overflowing from the middle of the range up. Staying under 25k tokens
163
+ at count=10 requires ≲**2.5k tokens (~10k English chars) per result**.
164
+ - **Codex: collides by policy, magnitude unpublished.** Codex truncates tool output by a
165
+ per-model, server-provided truncation policy (bytes or tokens — "Server-provided
166
+ truncation policy metadata for a model"), using **middle truncation**, with a 20%
167
+ serialization allowance added (`with_serialization_allowance` = policy × 1.2); no default
168
+ number is published in the repo or config reference (the only literal in source,
169
+ `bytes(10_000)`, is a `#[cfg(test)]` fixture). The per-tool override
170
+ `mcp_servers.<id>.tools.<tool>.output_token_limit` exists ("Token budget for one MCP
171
+ tool's output, before the standard 20% serialization allowance")
172
+ ([config reference](https://learn.chatgpt.com/docs/config-file/config-reference);
173
+ [mcp_types.rs at rust-v0.163.0](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/config/src/mcp_types.rs)).
174
+ [control-matrix.md](control-matrix.md) records Codex's 300 s MCP tool timeout — latency,
175
+ not size, is that host's documented ceiling.
176
+ - **The binding constraint is Pi's 20 KB**, roughly an order of magnitude tighter than
177
+ Claude Code's 25k-token default: a per-result content budget in the **~1k–2k char**
178
+ range keeps count=10 under every published cap; reusing the repo's fetch budget (16,000
179
+ chars) *per search result* would reach 160k chars per response — 8× Pi's cap and ~40k
180
+ tokens on Claude Code.
181
+ - **The repo's own 5 MiB `MAX_RESPONSE_BYTES` is not the binding constraint** at these
182
+ assumptions (10 results would need to average ~500 KB each to reach it), but note it
183
+ **rejects** oversized bodies with an error rather than truncating
184
+ ([transport.ts](../../src/core/transport.ts) `readResponseText`), and with no documented
185
+ server-side cap, a pathological page could trip it.
186
+
187
+ ## What this means for this repo
188
+
189
+ Fact-by-fact alignment with the current code. No recommendation is made here; the
190
+ maintainer decides.
191
+
192
+ - **The toggle point is one hardcoded literal.** `searchWeb` posts `need_content: false`
193
+ ([client.ts](../../src/core/client.ts)); everything else about a content-bearing mode
194
+ would be new surface.
195
+ - **Today's pipeline would silently drop the content.** `DeepSeekSearchResult`
196
+ ([types.ts](../../src/core/types.ts)) has no `content` field, and `processResults`
197
+ ([search.ts](../../src/core/search.ts)) maps `SearchResult.content` from
198
+ `summary ?? snippet` only — a `need_content` response's page bodies would be received and
199
+ discarded, unbilled-for-nothing but also unrendered.
200
+ - **The only per-result clamp in the search path is `maxSummaryChars` (1,200)** — a
201
+ summary/snippet budget. `fetchMaxChars` (16,000) and `formatFetchedContent`'s
202
+ truncation-marker logic apply exclusively to the fetch tool path. There is **no per-result
203
+ page-content budget knob of any kind**; the vendor table above shows the two shapes such
204
+ a knob takes elsewhere (Exa's per-result `maxCharacters`, Perplexity's per-result
205
+ `max_tokens_per_page`).
206
+ - **`MAX_RESPONSE_BYTES` (5 MiB) is the only existing guard a need_content response meets**,
207
+ and it errors rather than truncates — the one host cap it protects against is the
208
+ undocumented-content-size scenario at its pathological extreme.
209
+ - **Text rendering flattens structure.** `formatSearchResults` ([format.ts](../../src/core/format.ts))
210
+ renders each result's `content` with newlines replaced by single spaces — sized for a
211
+ 1,200-char summary. Multi-thousand-character Markdown bodies in the text content block
212
+ would lose heading/list structure; the `structuredContent` mirror preserves it as-is.
213
+ - **The two facts that make this decision unusual:** DeepSeek documents **no size bound and
214
+ no per-result budget knob** for `need_content` content (unlike Exa/Perplexity, whose
215
+ opt-in modes ship with budget parameters), and **no vendor but Jina ships
216
+ content-bearing search by default** — with Jina pinning count at 5, exactly the
217
+ count-shrinking direction the Pi 20 KB arithmetic above pushes toward.
218
+
219
+ ## Sources
220
+
221
+ - DeepSeek: [Search & Fetch guide (EN)](https://api-docs.deepseek.com/guides/search) ·
222
+ [网页搜索与抓取 (ZH)](https://api-docs.deepseek.com/zh-cn/guides/search) ·
223
+ [Pricing](https://api-docs.deepseek.com/quick_start/pricing) ·
224
+ [FAQ (static.deepseek.com/faq)](https://static.deepseek.com/faq/index.html?lang=en) —
225
+ question set inspected from the app bundle, 2026-10-23
226
+ - Tavily: [search endpoint](https://docs.tavily.com/documentation/api-reference/endpoint/search) ·
227
+ [extract endpoint](https://docs.tavily.com/documentation/api-reference/endpoint/extract)
228
+ - Exa: [search reference](https://exa.ai/docs/reference/search)
229
+ - You.com: [POST /v1/search reference](https://you.com/docs/api-reference/search/v1-search)
230
+ - Perplexity: [Search API reference](https://docs.perplexity.ai/api-reference/search-post) ·
231
+ [Agent API web_search tool](https://docs.perplexity.ai/docs/agent-api/tools/web-search)
232
+ - Anthropic: [web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) ·
233
+ [web fetch tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool)
234
+ - Firecrawl: [search endpoint](https://docs.firecrawl.dev/api-reference/endpoint/search)
235
+ - Jina: [reader README](https://github.com/jina-ai/reader/blob/main/README.md)
236
+ - Codex: [config reference](https://learn.chatgpt.com/docs/config-file/config-reference) ·
237
+ [mcp_types.rs at rust-v0.163.0](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/config/src/mcp_types.rs) ·
238
+ [openai_models.rs at rust-v0.163.0](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/protocol/src/openai_models.rs) ·
239
+ [output truncation lib at rust-v0.163.0](https://github.com/openai/codex/blob/rust-v0.163.0/codex-rs/utils/output-truncation/src/lib.rs)
240
+ - Repo: [search-api-defaults.md](search-api-defaults.md) ·
241
+ [control-matrix.md](control-matrix.md) ·
242
+ [constants.ts](../../src/core/constants.ts) · [client.ts](../../src/core/client.ts) ·
243
+ [search.ts](../../src/core/search.ts) · [format.ts](../../src/core/format.ts) ·
244
+ [transport.ts](../../src/core/transport.ts)
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
3
 
4
- // Global config: ~/.config/opencode/opencode.jsonc
5
- // Assumes the repo is cloned to ~/.config/opencode/vendor/deepseek-web-retrieval.
4
+ // Global config: ~/.config/opencode/opencode.jsonc (%APPDATA%\opencode\opencode.jsonc on Windows)
5
+ // Assumes the repo is cloned to the vendor/ directory beside the config
6
+ // (~/.config/opencode/vendor/deepseek-web-retrieval; %APPDATA%\opencode\vendor\... on Windows).
6
7
  "plugins": [
7
8
  {
8
9
  "package": "./vendor/deepseek-web-retrieval",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "deepseek-web-retrieval",
4
- "version": "0.6.0",
4
+ "version": "0.8.0",
5
5
  "description": "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  ],
49
49
  "scripts": {
50
50
  "lint": "biome check --error-on-warnings .",
51
+ "clean": "bun run scripts/clean.ts",
51
52
  "lint:ci": "biome ci --error-on-warnings .",
52
53
  "lint:fix": "biome check --write --error-on-warnings .",
53
54
  "format": "biome format --write .",
package/src/core/auth.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { API_KEY_ENV } from "./constants";
2
2
 
3
+ /** Shared missing-key guidance; every surface that needs a key prints this one message. */
4
+ export const MISSING_API_KEY_MESSAGE = `DeepSeek API key not found. Set ${API_KEY_ENV} in the MCP server's env configuration.`;
5
+
3
6
  export function apiKeyFromEnv(env: Readonly<Record<string, string | undefined>> = process.env): string | undefined {
4
7
  const key = env[API_KEY_ENV]?.trim();
5
8
  return key ? key : undefined;
@@ -1,6 +1,6 @@
1
- import { FETCH_ENDPOINT, MAX_QUERY_CHARS, SEARCH_ENDPOINT } from "./constants";
1
+ import { FETCH_ENDPOINT, MAX_ORI_QUESTION_CHARS, MAX_QUERY_CHARS, SEARCH_ENDPOINT } from "./constants";
2
2
  import { assertFetchHostPolicy } from "./domains";
3
- import { formatFetchedContent } from "./format";
3
+ import { formatFetchedContent, truncate } from "./format";
4
4
  import { buildQuery, processResults } from "./search";
5
5
  import { postJson } from "./transport";
6
6
  import type {
@@ -22,6 +22,16 @@ function effectiveTimeout(configuredMs: number, perCallMs: number | undefined):
22
22
  return perCallMs === undefined ? configuredMs : Math.min(configuredMs, perCallMs);
23
23
  }
24
24
 
25
+ /**
26
+ * The wire value for ori_question: absent/whitespace-only means "not
27
+ * provided"; longer values are clamped rather than rejected — the field is
28
+ * auxiliary and a hard error would punish the model for verbosity.
29
+ */
30
+ function oriQuestionValue(oriQuestion: string | undefined): string | undefined {
31
+ const trimmed = oriQuestion?.trim();
32
+ return trimmed ? truncate(trimmed, MAX_ORI_QUESTION_CHARS) : undefined;
33
+ }
34
+
25
35
  interface SearchResponse {
26
36
  readonly results?: unknown;
27
37
  }
@@ -41,10 +51,19 @@ export interface RequestOptions {
41
51
  readonly timeoutMs?: number;
42
52
  }
43
53
 
54
+ export interface SearchRequest extends RequestOptions {
55
+ /**
56
+ * Per-call only — the option-resolution pipeline never populates this. The
57
+ * user's original question behind the search, sent as DeepSeek's optional
58
+ * ori_question to sharpen intent; adapters fill it from tool input.
59
+ */
60
+ readonly oriQuestion?: string;
61
+ }
62
+
44
63
  export async function searchWeb(
45
64
  query: string,
46
65
  options: SearchOptions,
47
- request: RequestOptions,
66
+ request: SearchRequest,
48
67
  ): Promise<SearchResult[]> {
49
68
  const trimmed = query.trim();
50
69
  if (!trimmed) throw new Error("DeepSeek search query must not be empty");
@@ -63,12 +82,15 @@ export async function searchWeb(
63
82
  }
64
83
 
65
84
  const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
85
+ const oriQuestion = oriQuestionValue(request.oriQuestion);
66
86
  const payload = await postJson<SearchResponse>(
67
87
  SEARCH_ENDPOINT,
68
88
  {
69
89
  query: builtQuery,
70
90
  count: options.count,
71
91
  need_content: false,
92
+ ...(oriQuestion === undefined ? {} : { ori_question: oriQuestion }),
93
+ ...(options.localization === undefined ? {} : { localization: options.localization }),
72
94
  },
73
95
  "DeepSeek Search",
74
96
  { ...request, timeoutMs: timeout },
@@ -129,7 +151,7 @@ export async function fetchDocument(
129
151
  export function createRetrievalClient(dependencies: { readonly fetchImpl?: typeof fetch } = {}) {
130
152
  const bind = (request: RequestOptions): RequestOptions => ({ ...request, ...dependencies });
131
153
  return {
132
- searchWeb: (query: string, options: SearchOptions, request: RequestOptions) =>
154
+ searchWeb: (query: string, options: SearchOptions, request: SearchRequest) =>
133
155
  searchWeb(query, options, bind(request)),
134
156
  fetchPage: (url: URL, options: FetchOptions, request: RequestOptions) => fetchPage(url, options, bind(request)),
135
157
  fetchDocument: (url: string | URL, options: FetchOptions, request: RequestOptions) =>